clearOrUseOutboundGroupSession method

Future<bool> clearOrUseOutboundGroupSession(
  1. String roomId, {
  2. bool wipe = false,
  3. bool use = true,
})

Clears the existing outboundGroupSession but first checks if the participating devices have been changed. Returns false if the session has not been cleared because it wasn't necessary. Otherwise returns true.

Implementation

Future<bool> clearOrUseOutboundGroupSession(
  String roomId, {
  bool wipe = false,
  bool use = true,
}) async {
  final room = client.getRoomById(roomId);
  final sess = getOutboundGroupSession(roomId);
  if (room == null || sess == null || sess.outboundGroupSession == null) {
    return true;
  }

  if (!wipe) {
    // first check if it needs to be rotated
    final encryptionContent =
        room.getState(EventTypes.Encryption)?.parsedRoomEncryptionContent;
    final maxMessages = encryptionContent?.rotationPeriodMsgs ?? 100;
    final maxAge = encryptionContent?.rotationPeriodMs ??
        604800000; // default of one week
    if ((sess.sentMessages ?? maxMessages) >= maxMessages ||
        sess.creationTime
            .add(Duration(milliseconds: maxAge))
            .isBefore(DateTime.now())) {
      wipe = true;
    }
  }

  final inboundSess = await loadInboundGroupSession(
    room.id,
    sess.outboundGroupSession!.session_id(),
  );
  if (inboundSess == null) {
    wipe = true;
  }

  if (!wipe) {
    // next check if the devices in the room changed
    final devicesToReceive = <DeviceKeys>[];
    final newDeviceKeys = await room.getUserDeviceKeys();
    final newDeviceKeyIds = _getDeviceKeyIdMap(newDeviceKeys);
    // first check for user differences
    final oldUserIds = Set.from(sess.devices.keys);
    final newUserIds = Set.from(newDeviceKeyIds.keys);
    if (oldUserIds.difference(newUserIds).isNotEmpty) {
      // a user left the room, we must wipe the session
      wipe = true;
    } else {
      final newUsers = newUserIds.difference(oldUserIds);
      if (newUsers.isNotEmpty) {
        // new user! Gotta send the megolm session to them
        devicesToReceive
            .addAll(newDeviceKeys.where((d) => newUsers.contains(d.userId)));
      }
      // okay, now we must test all the individual user devices, if anything new got blocked
      // or if we need to send to any new devices.
      // for this it is enough if we iterate over the old user Ids, as the new ones already have the needed keys in the list.
      // we also know that all the old user IDs appear in the old one, else we have already wiped the session
      for (final userId in oldUserIds) {
        final oldBlockedDevices = sess.devices.containsKey(userId)
            ? Set.from(
                sess.devices[userId]!.entries
                    .where((e) => e.value)
                    .map((e) => e.key),
              )
            : <String>{};
        final newBlockedDevices = newDeviceKeyIds.containsKey(userId)
            ? Set.from(
                newDeviceKeyIds[userId]!
                    .entries
                    .where((e) => e.value)
                    .map((e) => e.key),
              )
            : <String>{};
        // we don't really care about old devices that got dropped (deleted), we only care if new ones got added and if new ones got blocked
        // check if new devices got blocked
        if (newBlockedDevices.difference(oldBlockedDevices).isNotEmpty) {
          wipe = true;
          break;
        }
        // and now add all the new devices!
        final oldDeviceIds = sess.devices.containsKey(userId)
            ? Set.from(
                sess.devices[userId]!.entries
                    .where((e) => !e.value)
                    .map((e) => e.key),
              )
            : <String>{};
        final newDeviceIds = newDeviceKeyIds.containsKey(userId)
            ? Set.from(
                newDeviceKeyIds[userId]!
                    .entries
                    .where((e) => !e.value)
                    .map((e) => e.key),
              )
            : <String>{};

        // check if a device got removed
        if (oldDeviceIds.difference(newDeviceIds).isNotEmpty) {
          wipe = true;
          break;
        }

        // check if any new devices need keys
        final newDevices = newDeviceIds.difference(oldDeviceIds);
        if (newDeviceIds.isNotEmpty) {
          devicesToReceive.addAll(
            newDeviceKeys.where(
              (d) => d.userId == userId && newDevices.contains(d.deviceId),
            ),
          );
        }
      }
    }

    if (!wipe) {
      if (!use) {
        return false;
      }
      // okay, we use the outbound group session!
      sess.devices = newDeviceKeyIds;
      final rawSession = <String, dynamic>{
        'algorithm': AlgorithmTypes.megolmV1AesSha2,
        'room_id': room.id,
        'session_id': sess.outboundGroupSession!.session_id(),
        'session_key': sess.outboundGroupSession!.session_key(),
      };
      try {
        devicesToReceive.removeWhere((k) => !k.encryptToDevice);
        if (devicesToReceive.isNotEmpty) {
          // update allowedAtIndex
          for (final device in devicesToReceive) {
            inboundSess!.allowedAtIndex[device.userId] ??= <String, int>{};
            if (!inboundSess.allowedAtIndex[device.userId]!
                    .containsKey(device.curve25519Key) ||
                inboundSess.allowedAtIndex[device.userId]![
                        device.curve25519Key]! >
                    sess.outboundGroupSession!.message_index()) {
              inboundSess
                      .allowedAtIndex[device.userId]![device.curve25519Key!] =
                  sess.outboundGroupSession!.message_index();
            }
          }
          await client.database?.updateInboundGroupSessionAllowedAtIndex(
            json.encode(inboundSess!.allowedAtIndex),
            room.id,
            sess.outboundGroupSession!.session_id(),
          );
          // send out the key
          await client.sendToDeviceEncryptedChunked(
            devicesToReceive,
            EventTypes.RoomKey,
            rawSession,
          );
        }
      } catch (e, s) {
        Logs().e(
          '[LibOlm] Unable to re-send the session key at later index to new devices',
          e,
          s,
        );
      }
      return false;
    }
  }
  sess.dispose();
  _outboundGroupSessions.remove(roomId);
  await client.database?.removeOutboundGroupSession(roomId);
  return true;
}