pick the correct session when encrypting to_device events

This commit is contained in:
Sorunome 2020-06-23 10:14:19 +02:00
parent f79a85ca71
commit 68fac1e112
No known key found for this signature in database
GPG key ID: B19471D07FC9BE9C
6 changed files with 203 additions and 88 deletions

View file

@ -23,6 +23,7 @@ import 'package:famedlysdk/famedlysdk.dart';
import 'package:famedlysdk/matrix_api.dart';
import 'package:olm/olm.dart' as olm;
import './encryption.dart';
import './utils/olm_session.dart';
class OlmManager {
final Encryption encryption;
@ -43,8 +44,8 @@ class OlmManager {
OlmManager(this.encryption);
/// A map from Curve25519 identity keys to existing olm sessions.
Map<String, List<olm.Session>> get olmSessions => _olmSessions;
final Map<String, List<olm.Session>> _olmSessions = {};
Map<String, List<OlmSession>> get olmSessions => _olmSessions;
final Map<String, List<OlmSession>> _olmSessions = {};
Future<void> init(String olmAccount) async {
if (olmAccount == null) {
@ -204,25 +205,24 @@ class OlmManager {
}
}
void storeOlmSession(String curve25519IdentityKey, olm.Session session) {
void storeOlmSession(OlmSession session) {
if (client.database == null) {
return;
}
if (!_olmSessions.containsKey(curve25519IdentityKey)) {
_olmSessions[curve25519IdentityKey] = [];
if (!_olmSessions.containsKey(session.identityKey)) {
_olmSessions[session.identityKey] = [];
}
final ix = _olmSessions[curve25519IdentityKey]
.indexWhere((s) => s.session_id() == session.session_id());
final ix = _olmSessions[session.identityKey]
.indexWhere((s) => s.sessionId == session.sessionId);
if (ix == -1) {
// add a new session
_olmSessions[curve25519IdentityKey].add(session);
_olmSessions[session.identityKey].add(session);
} else {
// update an existing session
_olmSessions[curve25519IdentityKey][ix] = session;
_olmSessions[session.identityKey][ix] = session;
}
final pickle = session.pickle(client.userID);
client.database.storeOlmSession(
client.id, curve25519IdentityKey, session.session_id(), pickle);
client.database.storeOlmSession(client.id, session.identityKey,
session.sessionId, session.pickledSession, session.lastReceived);
}
ToDeviceEvent _decryptToDeviceEvent(ToDeviceEvent event) {
@ -245,14 +245,16 @@ class OlmManager {
var existingSessions = olmSessions[senderKey];
if (existingSessions != null) {
for (var session in existingSessions) {
if (type == 0 && session.matches_inbound(body) == true) {
plaintext = session.decrypt(type, body);
storeOlmSession(senderKey, session);
if (type == 0 && session.session.matches_inbound(body) == true) {
plaintext = session.session.decrypt(type, body);
session.lastReceived = DateTime.now();
storeOlmSession(session);
break;
} else if (type == 1) {
try {
plaintext = session.decrypt(type, body);
storeOlmSession(senderKey, session);
plaintext = session.session.decrypt(type, body);
session.lastReceived = DateTime.now();
storeOlmSession(session);
break;
} catch (_) {
plaintext = null;
@ -271,7 +273,13 @@ class OlmManager {
_olmAccount.remove_one_time_keys(newSession);
client.database?.updateClientKeys(pickledOlmAccount, client.id);
plaintext = newSession.decrypt(type, body);
storeOlmSession(senderKey, newSession);
storeOlmSession(OlmSession(
key: client.userID,
identityKey: identityKey,
sessionId: newSession.session_id(),
session: newSession,
lastReceived: DateTime.now(),
));
} catch (_) {
newSession?.free();
rethrow;
@ -299,6 +307,22 @@ class OlmManager {
);
}
Future<List<OlmSession>> getOlmSessionsFromDatabase(String senderKey) async {
if (client.database == null) {
return [];
}
final rows =
await client.database.dbGetOlmSessions(client.id, senderKey).get();
final res = <OlmSession>[];
for (final row in rows) {
final sess = OlmSession.fromDb(row, client.userID);
if (sess.isValid) {
res.add(sess);
}
}
return res;
}
Future<ToDeviceEvent> decryptToDeviceEvent(ToDeviceEvent event) async {
if (event.type != EventTypes.Encrypted) {
return event;
@ -308,8 +332,7 @@ class OlmManager {
if (client.database == null) {
return false;
}
final sessions = await client.database
.getSingleOlmSessions(client.id, senderKey, client.userID);
final sessions = await getOlmSessionsFromDatabase(senderKey);
if (sessions.isEmpty) {
return false; // okay, can't do anything
}
@ -352,11 +375,18 @@ class OlmManager {
fingerprintKey, deviceKey, userId, deviceId)) {
continue;
}
var session = olm.Session();
try {
var session = olm.Session();
session.create_outbound(_olmAccount, identityKey, deviceKey['key']);
await storeOlmSession(identityKey, session);
await storeOlmSession(OlmSession(
key: client.userID,
identityKey: identityKey,
sessionId: session.session_id(),
session: session,
lastReceived: null,
));
} catch (e) {
session.free();
print('[LibOlm] Could not create new outbound olm session: ' +
e.toString());
}
@ -369,14 +399,15 @@ class OlmManager {
DeviceKeys device, String type, Map<String, dynamic> payload) async {
var sess = olmSessions[device.curve25519Key];
if (sess == null || sess.isEmpty) {
final sessions = await client.database
.getSingleOlmSessions(client.id, device.curve25519Key, client.userID);
final sessions = await getOlmSessionsFromDatabase(device.curve25519Key);
if (sessions.isEmpty) {
throw ('No olm session found');
}
sess = _olmSessions[device.curve25519Key] = sessions;
}
sess.sort((a, b) => a.session_id().compareTo(b.session_id()));
sess.sort((a, b) => a.lastReceived == b.lastReceived
? a.sessionId.compareTo(b.sessionId)
: b.lastReceived.compareTo(a.lastReceived));
final fullPayload = {
'type': type,
'content': payload,
@ -385,8 +416,8 @@ class OlmManager {
'recipient': device.userId,
'recipient_keys': {'ed25519': device.ed25519Key},
};
final encryptResult = sess.first.encrypt(json.encode(fullPayload));
storeOlmSession(device.curve25519Key, sess.first);
final encryptResult = sess.first.session.encrypt(json.encode(fullPayload));
storeOlmSession(sess.first);
final encryptedBody = <String, dynamic>{
'algorithm': 'm.olm.v1.curve25519-aes-sha2',
'sender_key': identityKey,
@ -440,7 +471,7 @@ class OlmManager {
void dispose() {
for (final sessions in olmSessions.values) {
for (final sess in sessions) {
sess.free();
sess.dispose();
}
}
_olmAccount?.free();

View file

@ -0,0 +1,59 @@
/*
* Famedly Matrix SDK
* 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 <https://www.gnu.org/licenses/>.
*/
import 'package:olm/olm.dart' as olm;
import '../../src/database/database.dart' show DbOlmSessions;
class OlmSession {
String identityKey;
String sessionId;
olm.Session session;
DateTime lastReceived;
final String key;
String get pickledSession => session.pickle(key);
bool get isValid => session != null;
OlmSession({
this.key,
this.identityKey,
this.sessionId,
this.session,
this.lastReceived,
});
OlmSession.fromDb(DbOlmSessions dbEntry, String key) : key = key {
session = olm.Session();
try {
session.unpickle(key, dbEntry.pickle);
identityKey = dbEntry.identityKey;
sessionId = dbEntry.identityKey;
lastReceived =
dbEntry.lastReceived ?? DateTime.fromMillisecondsSinceEpoch(0);
assert(sessionId == session.session_id());
} catch (e) {
print('[LibOlm] Could not unpickle olm session: ' + e.toString());
dispose();
}
}
void dispose() {
session?.free();
session = null;
}
}

View file

@ -16,7 +16,7 @@ class Database extends _$Database {
Database(QueryExecutor e) : super(e);
@override
int get schemaVersion => 4;
int get schemaVersion => 5;
int get maxFileSize => 1 * 1024 * 1024;
@ -55,6 +55,10 @@ class Database extends _$Database {
'UPDATE user_device_keys SET outdated = true');
from++;
}
if (from == 4) {
await m.addColumn(olmSessions, olmSessions.lastReceived);
from++;
}
},
beforeOpen: (_) async {
if (executor.dialect == SqlDialect.sqlite) {
@ -114,22 +118,6 @@ class Database extends _$Database {
return res;
}
Future<List<olm.Session>> getSingleOlmSessions(
int clientId, String identityKey, String userId) async {
final rows = await dbGetOlmSessions(clientId, identityKey).get();
final res = <olm.Session>[];
for (final row in rows) {
try {
var session = olm.Session();
session.unpickle(userId, row.pickle);
res.add(session);
} catch (e) {
print('[LibOlm] Could not unpickle olm session: ' + e.toString());
}
}
return res;
}
Future<DbOutboundGroupSession> getDbOutboundGroupSession(
int clientId, String roomId) async {
final res = await dbGetOutboundGroupSession(clientId, roomId).get();

View file

@ -1391,17 +1391,20 @@ class DbOlmSessions extends DataClass implements Insertable<DbOlmSessions> {
final String identityKey;
final String sessionId;
final String pickle;
final DateTime lastReceived;
DbOlmSessions(
{@required this.clientId,
@required this.identityKey,
@required this.sessionId,
@required this.pickle});
@required this.pickle,
this.lastReceived});
factory DbOlmSessions.fromData(
Map<String, dynamic> data, GeneratedDatabase db,
{String prefix}) {
final effectivePrefix = prefix ?? '';
final intType = db.typeSystem.forDartType<int>();
final stringType = db.typeSystem.forDartType<String>();
final dateTimeType = db.typeSystem.forDartType<DateTime>();
return DbOlmSessions(
clientId:
intType.mapFromDatabaseResponse(data['${effectivePrefix}client_id']),
@ -1411,6 +1414,8 @@ class DbOlmSessions extends DataClass implements Insertable<DbOlmSessions> {
.mapFromDatabaseResponse(data['${effectivePrefix}session_id']),
pickle:
stringType.mapFromDatabaseResponse(data['${effectivePrefix}pickle']),
lastReceived: dateTimeType
.mapFromDatabaseResponse(data['${effectivePrefix}last_received']),
);
}
@override
@ -1428,6 +1433,9 @@ class DbOlmSessions extends DataClass implements Insertable<DbOlmSessions> {
if (!nullToAbsent || pickle != null) {
map['pickle'] = Variable<String>(pickle);
}
if (!nullToAbsent || lastReceived != null) {
map['last_received'] = Variable<DateTime>(lastReceived);
}
return map;
}
@ -1439,6 +1447,7 @@ class DbOlmSessions extends DataClass implements Insertable<DbOlmSessions> {
identityKey: serializer.fromJson<String>(json['identity_key']),
sessionId: serializer.fromJson<String>(json['session_id']),
pickle: serializer.fromJson<String>(json['pickle']),
lastReceived: serializer.fromJson<DateTime>(json['last_received']),
);
}
@override
@ -1449,6 +1458,7 @@ class DbOlmSessions extends DataClass implements Insertable<DbOlmSessions> {
'identity_key': serializer.toJson<String>(identityKey),
'session_id': serializer.toJson<String>(sessionId),
'pickle': serializer.toJson<String>(pickle),
'last_received': serializer.toJson<DateTime>(lastReceived),
};
}
@ -1456,12 +1466,14 @@ class DbOlmSessions extends DataClass implements Insertable<DbOlmSessions> {
{int clientId,
String identityKey,
String sessionId,
String pickle}) =>
String pickle,
DateTime lastReceived}) =>
DbOlmSessions(
clientId: clientId ?? this.clientId,
identityKey: identityKey ?? this.identityKey,
sessionId: sessionId ?? this.sessionId,
pickle: pickle ?? this.pickle,
lastReceived: lastReceived ?? this.lastReceived,
);
@override
String toString() {
@ -1469,14 +1481,19 @@ class DbOlmSessions extends DataClass implements Insertable<DbOlmSessions> {
..write('clientId: $clientId, ')
..write('identityKey: $identityKey, ')
..write('sessionId: $sessionId, ')
..write('pickle: $pickle')
..write('pickle: $pickle, ')
..write('lastReceived: $lastReceived')
..write(')'))
.toString();
}
@override
int get hashCode => $mrjf($mrjc(clientId.hashCode,
$mrjc(identityKey.hashCode, $mrjc(sessionId.hashCode, pickle.hashCode))));
int get hashCode => $mrjf($mrjc(
clientId.hashCode,
$mrjc(
identityKey.hashCode,
$mrjc(sessionId.hashCode,
$mrjc(pickle.hashCode, lastReceived.hashCode)))));
@override
bool operator ==(dynamic other) =>
identical(this, other) ||
@ -1484,7 +1501,8 @@ class DbOlmSessions extends DataClass implements Insertable<DbOlmSessions> {
other.clientId == this.clientId &&
other.identityKey == this.identityKey &&
other.sessionId == this.sessionId &&
other.pickle == this.pickle);
other.pickle == this.pickle &&
other.lastReceived == this.lastReceived);
}
class OlmSessionsCompanion extends UpdateCompanion<DbOlmSessions> {
@ -1492,17 +1510,20 @@ class OlmSessionsCompanion extends UpdateCompanion<DbOlmSessions> {
final Value<String> identityKey;
final Value<String> sessionId;
final Value<String> pickle;
final Value<DateTime> lastReceived;
const OlmSessionsCompanion({
this.clientId = const Value.absent(),
this.identityKey = const Value.absent(),
this.sessionId = const Value.absent(),
this.pickle = const Value.absent(),
this.lastReceived = const Value.absent(),
});
OlmSessionsCompanion.insert({
@required int clientId,
@required String identityKey,
@required String sessionId,
@required String pickle,
this.lastReceived = const Value.absent(),
}) : clientId = Value(clientId),
identityKey = Value(identityKey),
sessionId = Value(sessionId),
@ -1512,12 +1533,14 @@ class OlmSessionsCompanion extends UpdateCompanion<DbOlmSessions> {
Expression<String> identityKey,
Expression<String> sessionId,
Expression<String> pickle,
Expression<DateTime> lastReceived,
}) {
return RawValuesInsertable({
if (clientId != null) 'client_id': clientId,
if (identityKey != null) 'identity_key': identityKey,
if (sessionId != null) 'session_id': sessionId,
if (pickle != null) 'pickle': pickle,
if (lastReceived != null) 'last_received': lastReceived,
});
}
@ -1525,12 +1548,14 @@ class OlmSessionsCompanion extends UpdateCompanion<DbOlmSessions> {
{Value<int> clientId,
Value<String> identityKey,
Value<String> sessionId,
Value<String> pickle}) {
Value<String> pickle,
Value<DateTime> lastReceived}) {
return OlmSessionsCompanion(
clientId: clientId ?? this.clientId,
identityKey: identityKey ?? this.identityKey,
sessionId: sessionId ?? this.sessionId,
pickle: pickle ?? this.pickle,
lastReceived: lastReceived ?? this.lastReceived,
);
}
@ -1549,6 +1574,9 @@ class OlmSessionsCompanion extends UpdateCompanion<DbOlmSessions> {
if (pickle.present) {
map['pickle'] = Variable<String>(pickle.value);
}
if (lastReceived.present) {
map['last_received'] = Variable<DateTime>(lastReceived.value);
}
return map;
}
}
@ -1591,9 +1619,19 @@ class OlmSessions extends Table with TableInfo<OlmSessions, DbOlmSessions> {
$customConstraints: 'NOT NULL');
}
final VerificationMeta _lastReceivedMeta =
const VerificationMeta('lastReceived');
GeneratedDateTimeColumn _lastReceived;
GeneratedDateTimeColumn get lastReceived =>
_lastReceived ??= _constructLastReceived();
GeneratedDateTimeColumn _constructLastReceived() {
return GeneratedDateTimeColumn('last_received', $tableName, true,
$customConstraints: '');
}
@override
List<GeneratedColumn> get $columns =>
[clientId, identityKey, sessionId, pickle];
[clientId, identityKey, sessionId, pickle, lastReceived];
@override
OlmSessions get asDslTable => this;
@override
@ -1631,6 +1669,12 @@ class OlmSessions extends Table with TableInfo<OlmSessions, DbOlmSessions> {
} else if (isInserting) {
context.missing(_pickleMeta);
}
if (data.containsKey('last_received')) {
context.handle(
_lastReceivedMeta,
lastReceived.isAcceptableOrUnknown(
data['last_received'], _lastReceivedMeta));
}
return context;
}
@ -5520,6 +5564,7 @@ abstract class _$Database extends GeneratedDatabase {
identityKey: row.readString('identity_key'),
sessionId: row.readString('session_id'),
pickle: row.readString('pickle'),
lastReceived: row.readDateTime('last_received'),
);
}
@ -5543,15 +5588,16 @@ abstract class _$Database extends GeneratedDatabase {
}).map(_rowToDbOlmSessions);
}
Future<int> storeOlmSession(
int client_id, String identitiy_key, String session_id, String pickle) {
Future<int> storeOlmSession(int client_id, String identitiy_key,
String session_id, String pickle, DateTime last_received) {
return customInsert(
'INSERT OR REPLACE INTO olm_sessions (client_id, identity_key, session_id, pickle) VALUES (:client_id, :identitiy_key, :session_id, :pickle)',
'INSERT OR REPLACE INTO olm_sessions (client_id, identity_key, session_id, pickle, last_received) VALUES (:client_id, :identitiy_key, :session_id, :pickle, :last_received)',
variables: [
Variable.withInt(client_id),
Variable.withString(identitiy_key),
Variable.withString(session_id),
Variable.withString(pickle)
Variable.withString(pickle),
Variable.withDateTime(last_received)
],
updates: {olmSessions},
);

View file

@ -48,6 +48,7 @@ CREATE TABLE olm_sessions (
identity_key TEXT NOT NULL,
session_id TEXT NOT NULL,
pickle TEXT NOT NULL,
last_received DATETIME,
UNIQUE(client_id, identity_key, session_id)
) AS DbOlmSessions;
CREATE INDEX olm_sessions_index ON olm_sessions(client_id);
@ -177,7 +178,7 @@ getAllUserDeviceKeysKeys: SELECT * FROM user_device_keys_key WHERE client_id = :
getAllUserCrossSigningKeys: SELECT * FROM user_cross_signing_keys WHERE client_id = :client_id;
getAllOlmSessions: SELECT * FROM olm_sessions WHERE client_id = :client_id;
dbGetOlmSessions: SELECT * FROM olm_sessions WHERE client_id = :client_id AND identity_key = :identity_key;
storeOlmSession: INSERT OR REPLACE INTO olm_sessions (client_id, identity_key, session_id, pickle) VALUES (:client_id, :identitiy_key, :session_id, :pickle);
storeOlmSession: INSERT OR REPLACE INTO olm_sessions (client_id, identity_key, session_id, pickle, last_received) VALUES (:client_id, :identitiy_key, :session_id, :pickle, :last_received);
getAllOutboundGroupSessions: SELECT * FROM outbound_group_sessions WHERE client_id = :client_id;
dbGetOutboundGroupSession: SELECT * FROM outbound_group_sessions WHERE client_id = :client_id AND room_id = :room_id;
storeOutboundGroupSession: INSERT OR REPLACE INTO outbound_group_sessions (client_id, room_id, pickle, device_ids, creation_time, sent_messages) VALUES (:client_id, :room_id, :pickle, :device_ids, :creation_time, :sent_messages);

View file

@ -139,12 +139,10 @@ void test() async {
assert(testClientB
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
1);
assert(testClientA
.encryption.olmManager.olmSessions[testClientB.identityKey].first
.session_id() ==
testClientB
.encryption.olmManager.olmSessions[testClientA.identityKey].first
.session_id());
assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey]
.first.sessionId ==
testClientB.encryption.olmManager.olmSessions[testClientA.identityKey]
.first.sessionId);
assert(inviteRoom.client.encryption.keyManager
.getInboundGroupSession(inviteRoom.id, currentSessionIdA, '') !=
null);
@ -162,12 +160,10 @@ void test() async {
assert(testClientB
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
1);
assert(testClientA
.encryption.olmManager.olmSessions[testClientB.identityKey].first
.session_id() ==
testClientB
.encryption.olmManager.olmSessions[testClientA.identityKey].first
.session_id());
assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey]
.first.sessionId ==
testClientB.encryption.olmManager.olmSessions[testClientA.identityKey]
.first.sessionId);
assert(room.client.encryption.keyManager
.getOutboundGroupSession(room.id)
@ -231,24 +227,20 @@ void test() async {
assert(testClientB
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
1);
assert(testClientA
.encryption.olmManager.olmSessions[testClientB.identityKey].first
.session_id() ==
testClientB
.encryption.olmManager.olmSessions[testClientA.identityKey].first
.session_id());
assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey]
.first.sessionId ==
testClientB.encryption.olmManager.olmSessions[testClientA.identityKey]
.first.sessionId);
assert(testClientA
.encryption.olmManager.olmSessions[testClientC.identityKey].length ==
1);
assert(testClientC
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
1);
assert(testClientA
.encryption.olmManager.olmSessions[testClientC.identityKey].first
.session_id() ==
testClientC
.encryption.olmManager.olmSessions[testClientA.identityKey].first
.session_id());
assert(testClientA.encryption.olmManager.olmSessions[testClientC.identityKey]
.first.sessionId ==
testClientC.encryption.olmManager.olmSessions[testClientA.identityKey]
.first.sessionId);
assert(room.client.encryption.keyManager
.getOutboundGroupSession(room.id)
.outboundGroupSession
@ -281,12 +273,10 @@ void test() async {
assert(testClientB
.encryption.olmManager.olmSessions[testClientA.identityKey].length ==
1);
assert(testClientA
.encryption.olmManager.olmSessions[testClientB.identityKey].first
.session_id() ==
testClientB
.encryption.olmManager.olmSessions[testClientA.identityKey].first
.session_id());
assert(testClientA.encryption.olmManager.olmSessions[testClientB.identityKey]
.first.sessionId ==
testClientB.encryption.olmManager.olmSessions[testClientA.identityKey]
.first.sessionId);
assert(room.client.encryption.keyManager
.getOutboundGroupSession(room.id)
.outboundGroupSession