first SSSS stuff
This commit is contained in:
parent
1a8ddb2750
commit
280cd4fc16
|
@ -47,6 +47,7 @@ import 'package:pedantic/pedantic.dart';
|
||||||
|
|
||||||
import 'event.dart';
|
import 'event.dart';
|
||||||
import 'room.dart';
|
import 'room.dart';
|
||||||
|
import 'ssss.dart';
|
||||||
import 'sync/event_update.dart';
|
import 'sync/event_update.dart';
|
||||||
import 'sync/room_update.dart';
|
import 'sync/room_update.dart';
|
||||||
import 'sync/user_update.dart';
|
import 'sync/user_update.dart';
|
||||||
|
@ -79,6 +80,8 @@ class Client {
|
||||||
|
|
||||||
bool enableE2eeRecovery;
|
bool enableE2eeRecovery;
|
||||||
|
|
||||||
|
SSSS ssss;
|
||||||
|
|
||||||
/// Create a client
|
/// Create a client
|
||||||
/// clientName = unique identifier of this client
|
/// clientName = unique identifier of this client
|
||||||
/// debug: Print debug output?
|
/// debug: Print debug output?
|
||||||
|
@ -86,6 +89,7 @@ class Client {
|
||||||
/// enableE2eeRecovery: Enable additional logic to try to recover from bad e2ee sessions
|
/// enableE2eeRecovery: Enable additional logic to try to recover from bad e2ee sessions
|
||||||
Client(this.clientName,
|
Client(this.clientName,
|
||||||
{this.debug = false, this.database, this.enableE2eeRecovery = false}) {
|
{this.debug = false, this.database, this.enableE2eeRecovery = false}) {
|
||||||
|
ssss = SSSS(this);
|
||||||
onLoginStateChanged.stream.listen((loginState) {
|
onLoginStateChanged.stream.listen((loginState) {
|
||||||
print('LoginState: ${loginState.toString()}');
|
print('LoginState: ${loginState.toString()}');
|
||||||
});
|
});
|
||||||
|
@ -1146,6 +1150,9 @@ class Client {
|
||||||
if (toDeviceEvent.type.startsWith('m.key.verification.')) {
|
if (toDeviceEvent.type.startsWith('m.key.verification.')) {
|
||||||
_handleToDeviceKeyVerificationRequest(toDeviceEvent);
|
_handleToDeviceKeyVerificationRequest(toDeviceEvent);
|
||||||
}
|
}
|
||||||
|
if (toDeviceEvent.type.startsWith('m.secret.')) {
|
||||||
|
ssss.handleToDeviceEvent(toDeviceEvent);
|
||||||
|
}
|
||||||
onToDeviceEvent.add(toDeviceEvent);
|
onToDeviceEvent.add(toDeviceEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2013,12 +2020,16 @@ class Client {
|
||||||
Map<String, dynamic> message, {
|
Map<String, dynamic> message, {
|
||||||
bool encrypted = true,
|
bool encrypted = true,
|
||||||
List<User> toUsers,
|
List<User> toUsers,
|
||||||
|
bool onlyVerified = false,
|
||||||
}) async {
|
}) async {
|
||||||
if (encrypted && !encryptionEnabled) return;
|
if (encrypted && !encryptionEnabled) return;
|
||||||
// Don't send this message to blocked devices.
|
// Don't send this message to blocked devices, and if specified onlyVerified
|
||||||
|
// then only send it to verified devices
|
||||||
if (deviceKeys.isNotEmpty) {
|
if (deviceKeys.isNotEmpty) {
|
||||||
deviceKeys.removeWhere((DeviceKeys deviceKeys) =>
|
deviceKeys.removeWhere((DeviceKeys deviceKeys) =>
|
||||||
deviceKeys.blocked || deviceKeys.deviceId == deviceID);
|
deviceKeys.blocked ||
|
||||||
|
deviceKeys.deviceId == deviceID ||
|
||||||
|
(onlyVerified && !deviceKeys.verified));
|
||||||
if (deviceKeys.isEmpty) return;
|
if (deviceKeys.isEmpty) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,6 +46,7 @@ class Database extends _$Database {
|
||||||
if (from == 3) {
|
if (from == 3) {
|
||||||
await m.createTable(userCrossSigningKeys);
|
await m.createTable(userCrossSigningKeys);
|
||||||
await m.createIndex(userCrossSigningKeysIndex);
|
await m.createIndex(userCrossSigningKeysIndex);
|
||||||
|
await m.createTable(ssssCache);
|
||||||
// mark all keys as outdated so that the cross signing keys will be fetched
|
// mark all keys as outdated so that the cross signing keys will be fetched
|
||||||
await m.issueCustomQuery(
|
await m.issueCustomQuery(
|
||||||
'UPDATE user_device_keys SET outdated = true');
|
'UPDATE user_device_keys SET outdated = true');
|
||||||
|
@ -125,6 +126,14 @@ class Database extends _$Database {
|
||||||
return res.first;
|
return res.first;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<DbSSSSCache> getSSSSCache(int clientId, String type) async {
|
||||||
|
final res = await dbGetSSSSCache(clientId, type).get();
|
||||||
|
if (res.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return res.first;
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<sdk.Room>> getRoomList(sdk.Client client,
|
Future<List<sdk.Room>> getRoomList(sdk.Client client,
|
||||||
{bool onlyLeft = false}) async {
|
{bool onlyLeft = false}) async {
|
||||||
final res = await (select(rooms)
|
final res = await (select(rooms)
|
||||||
|
@ -416,10 +425,14 @@ class Database extends _$Database {
|
||||||
..where((r) => r.clientId.equals(clientId)))
|
..where((r) => r.clientId.equals(clientId)))
|
||||||
.go();
|
.go();
|
||||||
await (delete(olmSessions)..where((r) => r.clientId.equals(clientId))).go();
|
await (delete(olmSessions)..where((r) => r.clientId.equals(clientId))).go();
|
||||||
|
await (delete(userCrossSigningKeys)
|
||||||
|
..where((r) => r.clientId.equals(clientId)))
|
||||||
|
.go();
|
||||||
await (delete(userDeviceKeysKey)..where((r) => r.clientId.equals(clientId)))
|
await (delete(userDeviceKeysKey)..where((r) => r.clientId.equals(clientId)))
|
||||||
.go();
|
.go();
|
||||||
await (delete(userDeviceKeys)..where((r) => r.clientId.equals(clientId)))
|
await (delete(userDeviceKeys)..where((r) => r.clientId.equals(clientId)))
|
||||||
.go();
|
.go();
|
||||||
|
await (delete(ssssCache)..where((r) => r.clientId.equals(clientId))).go();
|
||||||
await (delete(clients)..where((r) => r.clientId.equals(clientId))).go();
|
await (delete(clients)..where((r) => r.clientId.equals(clientId))).go();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4801,6 +4801,263 @@ class Presences extends Table with TableInfo<Presences, DbPresence> {
|
||||||
bool get dontWriteConstraints => true;
|
bool get dontWriteConstraints => true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DbSSSSCache extends DataClass implements Insertable<DbSSSSCache> {
|
||||||
|
final int clientId;
|
||||||
|
final String type;
|
||||||
|
final String keyId;
|
||||||
|
final String content;
|
||||||
|
DbSSSSCache(
|
||||||
|
{@required this.clientId,
|
||||||
|
@required this.type,
|
||||||
|
@required this.keyId,
|
||||||
|
@required this.content});
|
||||||
|
factory DbSSSSCache.fromData(Map<String, dynamic> data, GeneratedDatabase db,
|
||||||
|
{String prefix}) {
|
||||||
|
final effectivePrefix = prefix ?? '';
|
||||||
|
final intType = db.typeSystem.forDartType<int>();
|
||||||
|
final stringType = db.typeSystem.forDartType<String>();
|
||||||
|
return DbSSSSCache(
|
||||||
|
clientId:
|
||||||
|
intType.mapFromDatabaseResponse(data['${effectivePrefix}client_id']),
|
||||||
|
type: stringType.mapFromDatabaseResponse(data['${effectivePrefix}type']),
|
||||||
|
keyId:
|
||||||
|
stringType.mapFromDatabaseResponse(data['${effectivePrefix}key_id']),
|
||||||
|
content:
|
||||||
|
stringType.mapFromDatabaseResponse(data['${effectivePrefix}content']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@override
|
||||||
|
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||||
|
final map = <String, Expression>{};
|
||||||
|
if (!nullToAbsent || clientId != null) {
|
||||||
|
map['client_id'] = Variable<int>(clientId);
|
||||||
|
}
|
||||||
|
if (!nullToAbsent || type != null) {
|
||||||
|
map['type'] = Variable<String>(type);
|
||||||
|
}
|
||||||
|
if (!nullToAbsent || keyId != null) {
|
||||||
|
map['key_id'] = Variable<String>(keyId);
|
||||||
|
}
|
||||||
|
if (!nullToAbsent || content != null) {
|
||||||
|
map['content'] = Variable<String>(content);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory DbSSSSCache.fromJson(Map<String, dynamic> json,
|
||||||
|
{ValueSerializer serializer}) {
|
||||||
|
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||||
|
return DbSSSSCache(
|
||||||
|
clientId: serializer.fromJson<int>(json['client_id']),
|
||||||
|
type: serializer.fromJson<String>(json['type']),
|
||||||
|
keyId: serializer.fromJson<String>(json['key_id']),
|
||||||
|
content: serializer.fromJson<String>(json['content']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson({ValueSerializer serializer}) {
|
||||||
|
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||||
|
return <String, dynamic>{
|
||||||
|
'client_id': serializer.toJson<int>(clientId),
|
||||||
|
'type': serializer.toJson<String>(type),
|
||||||
|
'key_id': serializer.toJson<String>(keyId),
|
||||||
|
'content': serializer.toJson<String>(content),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
DbSSSSCache copyWith(
|
||||||
|
{int clientId, String type, String keyId, String content}) =>
|
||||||
|
DbSSSSCache(
|
||||||
|
clientId: clientId ?? this.clientId,
|
||||||
|
type: type ?? this.type,
|
||||||
|
keyId: keyId ?? this.keyId,
|
||||||
|
content: content ?? this.content,
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('DbSSSSCache(')
|
||||||
|
..write('clientId: $clientId, ')
|
||||||
|
..write('type: $type, ')
|
||||||
|
..write('keyId: $keyId, ')
|
||||||
|
..write('content: $content')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => $mrjf($mrjc(clientId.hashCode,
|
||||||
|
$mrjc(type.hashCode, $mrjc(keyId.hashCode, content.hashCode))));
|
||||||
|
@override
|
||||||
|
bool operator ==(dynamic other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
(other is DbSSSSCache &&
|
||||||
|
other.clientId == this.clientId &&
|
||||||
|
other.type == this.type &&
|
||||||
|
other.keyId == this.keyId &&
|
||||||
|
other.content == this.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
class SsssCacheCompanion extends UpdateCompanion<DbSSSSCache> {
|
||||||
|
final Value<int> clientId;
|
||||||
|
final Value<String> type;
|
||||||
|
final Value<String> keyId;
|
||||||
|
final Value<String> content;
|
||||||
|
const SsssCacheCompanion({
|
||||||
|
this.clientId = const Value.absent(),
|
||||||
|
this.type = const Value.absent(),
|
||||||
|
this.keyId = const Value.absent(),
|
||||||
|
this.content = const Value.absent(),
|
||||||
|
});
|
||||||
|
SsssCacheCompanion.insert({
|
||||||
|
@required int clientId,
|
||||||
|
@required String type,
|
||||||
|
@required String keyId,
|
||||||
|
@required String content,
|
||||||
|
}) : clientId = Value(clientId),
|
||||||
|
type = Value(type),
|
||||||
|
keyId = Value(keyId),
|
||||||
|
content = Value(content);
|
||||||
|
static Insertable<DbSSSSCache> custom({
|
||||||
|
Expression<int> clientId,
|
||||||
|
Expression<String> type,
|
||||||
|
Expression<String> keyId,
|
||||||
|
Expression<String> content,
|
||||||
|
}) {
|
||||||
|
return RawValuesInsertable({
|
||||||
|
if (clientId != null) 'client_id': clientId,
|
||||||
|
if (type != null) 'type': type,
|
||||||
|
if (keyId != null) 'key_id': keyId,
|
||||||
|
if (content != null) 'content': content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
SsssCacheCompanion copyWith(
|
||||||
|
{Value<int> clientId,
|
||||||
|
Value<String> type,
|
||||||
|
Value<String> keyId,
|
||||||
|
Value<String> content}) {
|
||||||
|
return SsssCacheCompanion(
|
||||||
|
clientId: clientId ?? this.clientId,
|
||||||
|
type: type ?? this.type,
|
||||||
|
keyId: keyId ?? this.keyId,
|
||||||
|
content: content ?? this.content,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||||
|
final map = <String, Expression>{};
|
||||||
|
if (clientId.present) {
|
||||||
|
map['client_id'] = Variable<int>(clientId.value);
|
||||||
|
}
|
||||||
|
if (type.present) {
|
||||||
|
map['type'] = Variable<String>(type.value);
|
||||||
|
}
|
||||||
|
if (keyId.present) {
|
||||||
|
map['key_id'] = Variable<String>(keyId.value);
|
||||||
|
}
|
||||||
|
if (content.present) {
|
||||||
|
map['content'] = Variable<String>(content.value);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SsssCache extends Table with TableInfo<SsssCache, DbSSSSCache> {
|
||||||
|
final GeneratedDatabase _db;
|
||||||
|
final String _alias;
|
||||||
|
SsssCache(this._db, [this._alias]);
|
||||||
|
final VerificationMeta _clientIdMeta = const VerificationMeta('clientId');
|
||||||
|
GeneratedIntColumn _clientId;
|
||||||
|
GeneratedIntColumn get clientId => _clientId ??= _constructClientId();
|
||||||
|
GeneratedIntColumn _constructClientId() {
|
||||||
|
return GeneratedIntColumn('client_id', $tableName, false,
|
||||||
|
$customConstraints: 'NOT NULL REFERENCES clients(client_id)');
|
||||||
|
}
|
||||||
|
|
||||||
|
final VerificationMeta _typeMeta = const VerificationMeta('type');
|
||||||
|
GeneratedTextColumn _type;
|
||||||
|
GeneratedTextColumn get type => _type ??= _constructType();
|
||||||
|
GeneratedTextColumn _constructType() {
|
||||||
|
return GeneratedTextColumn('type', $tableName, false,
|
||||||
|
$customConstraints: 'NOT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
final VerificationMeta _keyIdMeta = const VerificationMeta('keyId');
|
||||||
|
GeneratedTextColumn _keyId;
|
||||||
|
GeneratedTextColumn get keyId => _keyId ??= _constructKeyId();
|
||||||
|
GeneratedTextColumn _constructKeyId() {
|
||||||
|
return GeneratedTextColumn('key_id', $tableName, false,
|
||||||
|
$customConstraints: 'NOT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
final VerificationMeta _contentMeta = const VerificationMeta('content');
|
||||||
|
GeneratedTextColumn _content;
|
||||||
|
GeneratedTextColumn get content => _content ??= _constructContent();
|
||||||
|
GeneratedTextColumn _constructContent() {
|
||||||
|
return GeneratedTextColumn('content', $tableName, false,
|
||||||
|
$customConstraints: 'NOT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<GeneratedColumn> get $columns => [clientId, type, keyId, content];
|
||||||
|
@override
|
||||||
|
SsssCache get asDslTable => this;
|
||||||
|
@override
|
||||||
|
String get $tableName => _alias ?? 'ssss_cache';
|
||||||
|
@override
|
||||||
|
final String actualTableName = 'ssss_cache';
|
||||||
|
@override
|
||||||
|
VerificationContext validateIntegrity(Insertable<DbSSSSCache> instance,
|
||||||
|
{bool isInserting = false}) {
|
||||||
|
final context = VerificationContext();
|
||||||
|
final data = instance.toColumns(true);
|
||||||
|
if (data.containsKey('client_id')) {
|
||||||
|
context.handle(_clientIdMeta,
|
||||||
|
clientId.isAcceptableOrUnknown(data['client_id'], _clientIdMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_clientIdMeta);
|
||||||
|
}
|
||||||
|
if (data.containsKey('type')) {
|
||||||
|
context.handle(
|
||||||
|
_typeMeta, type.isAcceptableOrUnknown(data['type'], _typeMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_typeMeta);
|
||||||
|
}
|
||||||
|
if (data.containsKey('key_id')) {
|
||||||
|
context.handle(
|
||||||
|
_keyIdMeta, keyId.isAcceptableOrUnknown(data['key_id'], _keyIdMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_keyIdMeta);
|
||||||
|
}
|
||||||
|
if (data.containsKey('content')) {
|
||||||
|
context.handle(_contentMeta,
|
||||||
|
content.isAcceptableOrUnknown(data['content'], _contentMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_contentMeta);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<GeneratedColumn> get $primaryKey => <GeneratedColumn>{};
|
||||||
|
@override
|
||||||
|
DbSSSSCache map(Map<String, dynamic> data, {String tablePrefix}) {
|
||||||
|
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null;
|
||||||
|
return DbSSSSCache.fromData(data, _db, prefix: effectivePrefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SsssCache createAlias(String alias) {
|
||||||
|
return SsssCache(_db, alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<String> get customConstraints => const ['UNIQUE(client_id, type)'];
|
||||||
|
@override
|
||||||
|
bool get dontWriteConstraints => true;
|
||||||
|
}
|
||||||
|
|
||||||
class DbFile extends DataClass implements Insertable<DbFile> {
|
class DbFile extends DataClass implements Insertable<DbFile> {
|
||||||
final String mxcUri;
|
final String mxcUri;
|
||||||
final Uint8List bytes;
|
final Uint8List bytes;
|
||||||
|
@ -5087,6 +5344,8 @@ abstract class _$Database extends GeneratedDatabase {
|
||||||
Index _presencesIndex;
|
Index _presencesIndex;
|
||||||
Index get presencesIndex => _presencesIndex ??= Index('presences_index',
|
Index get presencesIndex => _presencesIndex ??= Index('presences_index',
|
||||||
'CREATE INDEX presences_index ON presences(client_id);');
|
'CREATE INDEX presences_index ON presences(client_id);');
|
||||||
|
SsssCache _ssssCache;
|
||||||
|
SsssCache get ssssCache => _ssssCache ??= SsssCache(this);
|
||||||
Files _files;
|
Files _files;
|
||||||
Files get files => _files ??= Files(this);
|
Files get files => _files ??= Files(this);
|
||||||
DbClient _rowToDbClient(QueryRow row) {
|
DbClient _rowToDbClient(QueryRow row) {
|
||||||
|
@ -5498,6 +5757,36 @@ abstract class _$Database extends GeneratedDatabase {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<int> storeSSSSCache(
|
||||||
|
int client_id, String type, String key_id, String content) {
|
||||||
|
return customInsert(
|
||||||
|
'INSERT OR REPLACE INTO ssss_cache (client_id, type, key_id, content) VALUES (:client_id, :type, :key_id, :content)',
|
||||||
|
variables: [
|
||||||
|
Variable.withInt(client_id),
|
||||||
|
Variable.withString(type),
|
||||||
|
Variable.withString(key_id),
|
||||||
|
Variable.withString(content)
|
||||||
|
],
|
||||||
|
updates: {ssssCache},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DbSSSSCache _rowToDbSSSSCache(QueryRow row) {
|
||||||
|
return DbSSSSCache(
|
||||||
|
clientId: row.readInt('client_id'),
|
||||||
|
type: row.readString('type'),
|
||||||
|
keyId: row.readString('key_id'),
|
||||||
|
content: row.readString('content'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Selectable<DbSSSSCache> dbGetSSSSCache(int client_id, String type) {
|
||||||
|
return customSelect(
|
||||||
|
'SELECT * FROM ssss_cache WHERE client_id = :client_id AND type = :type',
|
||||||
|
variables: [Variable.withInt(client_id), Variable.withString(type)],
|
||||||
|
readsFrom: {ssssCache}).map(_rowToDbSSSSCache);
|
||||||
|
}
|
||||||
|
|
||||||
Future<int> insertClient(
|
Future<int> insertClient(
|
||||||
String name,
|
String name,
|
||||||
String homeserver_url,
|
String homeserver_url,
|
||||||
|
@ -5924,6 +6213,7 @@ abstract class _$Database extends GeneratedDatabase {
|
||||||
roomAccountDataIndex,
|
roomAccountDataIndex,
|
||||||
presences,
|
presences,
|
||||||
presencesIndex,
|
presencesIndex,
|
||||||
|
ssssCache,
|
||||||
files
|
files
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,6 +74,14 @@ CREATE TABLE inbound_group_sessions (
|
||||||
) AS DbInboundGroupSession;
|
) AS DbInboundGroupSession;
|
||||||
CREATE INDEX inbound_group_sessions_index ON inbound_group_sessions(client_id);
|
CREATE INDEX inbound_group_sessions_index ON inbound_group_sessions(client_id);
|
||||||
|
|
||||||
|
CREATE TABLE ssss_cache (
|
||||||
|
client_id INTEGER NOT NULL REFERENCES clients(client_id),
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
key_id TEXT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
UNIQUE(client_id, type)
|
||||||
|
) AS DbSSSSCache;
|
||||||
|
|
||||||
CREATE TABLE rooms (
|
CREATE TABLE rooms (
|
||||||
client_id INTEGER NOT NULL REFERENCES clients(client_id),
|
client_id INTEGER NOT NULL REFERENCES clients(client_id),
|
||||||
room_id TEXT NOT NULL,
|
room_id TEXT NOT NULL,
|
||||||
|
@ -186,6 +194,8 @@ setVerifiedUserCrossSigningKey: UPDATE user_cross_signing_keys SET verified = :v
|
||||||
setBlockedUserCrossSigningKey: UPDATE user_cross_signing_keys SET blocked = :blocked WHERE client_id = :client_id AND user_id = :user_id AND public_key = :public_key;
|
setBlockedUserCrossSigningKey: UPDATE user_cross_signing_keys SET blocked = :blocked WHERE client_id = :client_id AND user_id = :user_id AND public_key = :public_key;
|
||||||
storeUserCrossSigningKey: INSERT OR REPLACE INTO user_cross_signing_keys (client_id, user_id, public_key, content, verified, blocked) VALUES (:client_id, :user_id, :public_key, :content, :verified, :blocked);
|
storeUserCrossSigningKey: INSERT OR REPLACE INTO user_cross_signing_keys (client_id, user_id, public_key, content, verified, blocked) VALUES (:client_id, :user_id, :public_key, :content, :verified, :blocked);
|
||||||
removeUserCrossSigningKey: DELETE FROM user_cross_signing_keys WHERE client_id = :client_id AND user_id = :user_id AND public_key = :public_key;
|
removeUserCrossSigningKey: DELETE FROM user_cross_signing_keys WHERE client_id = :client_id AND user_id = :user_id AND public_key = :public_key;
|
||||||
|
storeSSSSCache: INSERT OR REPLACE INTO ssss_cache (client_id, type, key_id, content) VALUES (:client_id, :type, :key_id, :content);
|
||||||
|
dbGetSSSSCache: SELECT * FROM ssss_cache WHERE client_id = :client_id AND type = :type;
|
||||||
insertClient: INSERT INTO clients (name, homeserver_url, token, user_id, device_id, device_name, prev_batch, olm_account) VALUES (:name, :homeserver_url, :token, :user_id, :device_id, :device_name, :prev_batch, :olm_account);
|
insertClient: INSERT INTO clients (name, homeserver_url, token, user_id, device_id, device_name, prev_batch, olm_account) VALUES (:name, :homeserver_url, :token, :user_id, :device_id, :device_name, :prev_batch, :olm_account);
|
||||||
ensureRoomExists: INSERT OR IGNORE INTO rooms (client_id, room_id, membership) VALUES (:client_id, :room_id, :membership);
|
ensureRoomExists: INSERT OR IGNORE INTO rooms (client_id, room_id, membership) VALUES (:client_id, :room_id, :membership);
|
||||||
setRoomPrevBatch: UPDATE rooms SET prev_batch = :prev_batch WHERE client_id = :client_id AND room_id = :room_id;
|
setRoomPrevBatch: UPDATE rooms SET prev_batch = :prev_batch WHERE client_id = :client_id AND room_id = :room_id;
|
||||||
|
|
456
lib/src/ssss.dart
Normal file
456
lib/src/ssss.dart
Normal file
|
@ -0,0 +1,456 @@
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:encrypt/encrypt.dart';
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
|
import "package:base58check/base58.dart";
|
||||||
|
import 'package:password_hash/password_hash.dart';
|
||||||
|
import 'package:random_string/random_string.dart';
|
||||||
|
|
||||||
|
import 'client.dart';
|
||||||
|
import 'account_data.dart';
|
||||||
|
import 'utils/device_keys_list.dart';
|
||||||
|
import 'utils/to_device_event.dart';
|
||||||
|
|
||||||
|
const CACHE_TYPES = <String>[
|
||||||
|
'm.cross_signing.self_signing',
|
||||||
|
'm.cross_signing.user_signing',
|
||||||
|
'm.megolm_backup.v1'
|
||||||
|
];
|
||||||
|
const ZERO_STR =
|
||||||
|
'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00';
|
||||||
|
const BASE58_ALPHABET =
|
||||||
|
'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
||||||
|
const base58 = const Base58Codec(BASE58_ALPHABET);
|
||||||
|
const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01];
|
||||||
|
const OLM_PRIVATE_KEY_LENGTH = 32; // TODO: fetch from dart-olm
|
||||||
|
const AES_BLOCKSIZE = 16;
|
||||||
|
|
||||||
|
class SSSS {
|
||||||
|
final Client client;
|
||||||
|
final pendingShareRequests = <String, _ShareRequest>{};
|
||||||
|
SSSS(this.client);
|
||||||
|
|
||||||
|
static _DerivedKeys deriveKeys(Uint8List key, String name) {
|
||||||
|
final zerosalt = Uint8List(8);
|
||||||
|
final prk = Hmac(sha256, zerosalt).convert(key);
|
||||||
|
final b = Uint8List(1);
|
||||||
|
b[0] = 1;
|
||||||
|
final aesKey = Hmac(sha256, prk.bytes).convert(utf8.encode(name) + b);
|
||||||
|
b[0] = 2;
|
||||||
|
final a = aesKey.bytes + utf8.encode(name) + b;
|
||||||
|
final hmacKey =
|
||||||
|
Hmac(sha256, prk.bytes).convert(aesKey.bytes + utf8.encode(name) + b);
|
||||||
|
return _DerivedKeys(aesKey: aesKey.bytes, hmacKey: hmacKey.bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
static _Encrypted encryptAes(String data, Uint8List key, String name,
|
||||||
|
[String ivStr]) {
|
||||||
|
Uint8List iv;
|
||||||
|
if (ivStr != null) {
|
||||||
|
iv = base64.decode(ivStr);
|
||||||
|
} else {
|
||||||
|
iv = Uint8List.fromList(SecureRandom(16).bytes);
|
||||||
|
}
|
||||||
|
// we need to clear bit 63 of the IV
|
||||||
|
iv[8] &= 0x7f;
|
||||||
|
|
||||||
|
final keys = deriveKeys(key, name);
|
||||||
|
|
||||||
|
// workaround for https://github.com/leocavalcante/encrypt/issues/136
|
||||||
|
var plain = Uint8List.fromList(utf8.encode(data));
|
||||||
|
final bytesMissing = AES_BLOCKSIZE - (plain.lengthInBytes % AES_BLOCKSIZE);
|
||||||
|
if (bytesMissing != AES_BLOCKSIZE) {
|
||||||
|
// we want to be able to modify it
|
||||||
|
final oldPlain = plain;
|
||||||
|
plain = Uint8List(plain.lengthInBytes + bytesMissing);
|
||||||
|
for (var i = 0; i < oldPlain.lengthInBytes; i++) {
|
||||||
|
plain[i] = oldPlain[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var ciphertext = AES(Key(keys.aesKey), mode: AESMode.ctr, padding: null)
|
||||||
|
.encrypt(plain, iv: IV(iv)).bytes;
|
||||||
|
if (bytesMissing != AES_BLOCKSIZE) {
|
||||||
|
// chop off those extra bytes again
|
||||||
|
ciphertext = ciphertext.sublist(0, plain.length - bytesMissing);
|
||||||
|
}
|
||||||
|
|
||||||
|
final hmac = Hmac(sha256, keys.hmacKey).convert(ciphertext);
|
||||||
|
|
||||||
|
return _Encrypted(
|
||||||
|
iv: base64.encode(iv),
|
||||||
|
ciphertext: base64.encode(ciphertext),
|
||||||
|
mac: base64.encode(hmac.bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
static String decryptAes(_Encrypted data, Uint8List key, String name) {
|
||||||
|
final keys = deriveKeys(key, name);
|
||||||
|
final hmac = base64
|
||||||
|
.encode(Hmac(sha256, keys.hmacKey)
|
||||||
|
.convert(base64.decode(data.ciphertext))
|
||||||
|
.bytes)
|
||||||
|
.replaceAll(RegExp(r'=+$'), '');
|
||||||
|
if (hmac != data.mac.replaceAll(RegExp(r'=+$'), '')) {
|
||||||
|
throw 'Bad MAC';
|
||||||
|
}
|
||||||
|
// workaround for https://github.com/leocavalcante/encrypt/issues/136
|
||||||
|
var cipher = base64.decode(data.ciphertext);
|
||||||
|
final bytesMissing = AES_BLOCKSIZE - (cipher.lengthInBytes % AES_BLOCKSIZE);
|
||||||
|
if (bytesMissing != AES_BLOCKSIZE) {
|
||||||
|
// we want to be able to modify it
|
||||||
|
final oldCipher = cipher;
|
||||||
|
cipher = Uint8List(cipher.lengthInBytes + bytesMissing);
|
||||||
|
for (var i = 0; i < oldCipher.lengthInBytes; i++) {
|
||||||
|
cipher[i] = oldCipher[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final decipher = AES(Key(keys.aesKey), mode: AESMode.ctr, padding: null).decrypt(
|
||||||
|
Encrypted(cipher),
|
||||||
|
iv: IV(base64.decode(data.iv)));
|
||||||
|
if (bytesMissing != AES_BLOCKSIZE) {
|
||||||
|
// chop off those extra bytes again
|
||||||
|
return String.fromCharCodes(decipher.sublist(0, decipher.length - bytesMissing));
|
||||||
|
}
|
||||||
|
return String.fromCharCodes(decipher);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Uint8List decodeRecoveryKey(String recoveryKey) {
|
||||||
|
final result = base58.decode(recoveryKey.replaceAll(' ', ''));
|
||||||
|
|
||||||
|
var parity = 0;
|
||||||
|
for (final b in result) {
|
||||||
|
parity ^= b;
|
||||||
|
}
|
||||||
|
if (parity != 0) {
|
||||||
|
throw 'Incorrect parity';
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < OLM_RECOVERY_KEY_PREFIX.length; i++) {
|
||||||
|
if (result[i] != OLM_RECOVERY_KEY_PREFIX[i]) {
|
||||||
|
throw 'Incorrect prefix';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.length !=
|
||||||
|
OLM_RECOVERY_KEY_PREFIX.length + OLM_PRIVATE_KEY_LENGTH + 1) {
|
||||||
|
throw 'Incorrect length';
|
||||||
|
}
|
||||||
|
|
||||||
|
return Uint8List.fromList(result.sublist(OLM_RECOVERY_KEY_PREFIX.length,
|
||||||
|
OLM_RECOVERY_KEY_PREFIX.length + OLM_PRIVATE_KEY_LENGTH));
|
||||||
|
}
|
||||||
|
|
||||||
|
static Uint8List keyFromPassword(String password, _PasswordInfo info) {
|
||||||
|
if (info.algorithm != 'm.pbkdf2') {
|
||||||
|
throw 'Unknown algorithm';
|
||||||
|
}
|
||||||
|
final generator = PBKDF2(hashAlgorithm: sha512);
|
||||||
|
return Uint8List.fromList(generator.generateKey(
|
||||||
|
password, info.salt, info.iterations, info.bits != null ? info.bits / 8 : 32));
|
||||||
|
}
|
||||||
|
|
||||||
|
String get defaultKeyId {
|
||||||
|
final keyData = client.accountData['m.secret_storage.default_key'];
|
||||||
|
if (keyData == null || !(keyData.content['key'] is String)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return keyData.content['key'];
|
||||||
|
}
|
||||||
|
|
||||||
|
AccountData getKey(String keyId) {
|
||||||
|
return client.accountData['m.secret_storage.key.${keyId}'];
|
||||||
|
}
|
||||||
|
|
||||||
|
bool checkKey(Uint8List key, AccountData keyData) {
|
||||||
|
final info = keyData.content;
|
||||||
|
if (info['algorithm'] == 'm.secret_storage.v1.aes-hmac-sha2') {
|
||||||
|
if ((info['mac'] is String) && (info['iv'] is String)) {
|
||||||
|
final encrypted = encryptAes(ZERO_STR, key, '', info['iv']);
|
||||||
|
return info['mac'].replaceAll(RegExp(r'=+$'), '') ==
|
||||||
|
encrypted.mac.replaceAll(RegExp(r'=+$'), '');
|
||||||
|
} else {
|
||||||
|
// no real information about the key, assume it is valid
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw 'Unknown Algorithm';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> getCached(String type) async {
|
||||||
|
if (client.database == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final ret = await client.database.getSSSSCache(client.id, type);
|
||||||
|
if (ret == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// check if it is still valid
|
||||||
|
final keys = keyIdsFromType(type);
|
||||||
|
if (keys.contains(ret.keyId)) {
|
||||||
|
return ret.content;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> getStored(String type, String keyId, Uint8List key) async {
|
||||||
|
final secretInfo = client.accountData[type];
|
||||||
|
if (secretInfo == null) {
|
||||||
|
throw 'Not found';
|
||||||
|
}
|
||||||
|
if (!(secretInfo.content['encrypted'] is Map)) {
|
||||||
|
throw 'Content is not encrypted';
|
||||||
|
}
|
||||||
|
if (!(secretInfo.content['encrypted'][keyId] is Map)) {
|
||||||
|
throw 'Wrong / unknown key';
|
||||||
|
}
|
||||||
|
final enc = secretInfo.content['encrypted'][keyId];
|
||||||
|
final encryptInfo = _Encrypted(
|
||||||
|
iv: enc['iv'], ciphertext: enc['ciphertext'], mac: enc['mac']);
|
||||||
|
final decrypted = decryptAes(encryptInfo, key, type);
|
||||||
|
if (CACHE_TYPES.contains(type) && client.database != null) {
|
||||||
|
// cache the thing
|
||||||
|
await client.database.storeSSSSCache(client.id, type, keyId, decrypted);
|
||||||
|
}
|
||||||
|
return decrypted;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> store(
|
||||||
|
String type, String secret, String keyId, Uint8List key) async {
|
||||||
|
final encrypted = encryptAes(secret, key, type);
|
||||||
|
final content = <String, dynamic>{
|
||||||
|
'encrypted': <String, dynamic>{},
|
||||||
|
};
|
||||||
|
content['encrypted'][keyId] = <String, dynamic>{
|
||||||
|
'iv': encrypted.iv,
|
||||||
|
'ciphertext': encrypted.ciphertext,
|
||||||
|
'mac': encrypted.mac,
|
||||||
|
};
|
||||||
|
// store the thing in your account data
|
||||||
|
await client.jsonRequest(
|
||||||
|
type: HTTPType.PUT,
|
||||||
|
action: '/client/r0/user/${client.userID}/account_data/${type}',
|
||||||
|
data: content,
|
||||||
|
);
|
||||||
|
if (CACHE_TYPES.contains(type) && client.database != null) {
|
||||||
|
// cache the thing
|
||||||
|
await client.database.storeSSSSCache(client.id, type, keyId, secret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> maybeRequestAll(List<DeviceKeys> devices) async {
|
||||||
|
for (final type in CACHE_TYPES) {
|
||||||
|
final secret = await getCached(type);
|
||||||
|
if (secret == null) {
|
||||||
|
await request(type, devices);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> request(String type, List<DeviceKeys> devices) async {
|
||||||
|
// only send to own, verified devices
|
||||||
|
print('[SSSS] Requesting type ${type}...');
|
||||||
|
devices.removeWhere((DeviceKeys d) =>
|
||||||
|
d.userId != client.userID ||
|
||||||
|
!d.verified ||
|
||||||
|
d.blocked ||
|
||||||
|
d.deviceId == client.deviceID);
|
||||||
|
if (devices.isEmpty) {
|
||||||
|
print('[SSSS] Warn: No devices');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final requestId =
|
||||||
|
randomString(512) + DateTime.now().millisecondsSinceEpoch.toString();
|
||||||
|
final request = _ShareRequest(
|
||||||
|
requestId: requestId,
|
||||||
|
type: type,
|
||||||
|
devices: devices,
|
||||||
|
);
|
||||||
|
pendingShareRequests[requestId] = request;
|
||||||
|
await client.sendToDevice(devices, 'm.secret.request', {
|
||||||
|
'action': 'request',
|
||||||
|
'requesting_device_id': client.deviceID,
|
||||||
|
'request_id': requestId,
|
||||||
|
'name': type,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> handleToDeviceEvent(ToDeviceEvent event) async {
|
||||||
|
if (event.type == 'm.secret.request') {
|
||||||
|
// got a request to share a secret
|
||||||
|
print('[SSSS] Received sharing request...');
|
||||||
|
if (event.sender != client.userID ||
|
||||||
|
!client.userDeviceKeys.containsKey(client.userID)) {
|
||||||
|
print('[SSSS] Not sent by us');
|
||||||
|
return; // we aren't asking for it ourselves, so ignore
|
||||||
|
}
|
||||||
|
if (event.content['action'] != 'request') {
|
||||||
|
print('[SSSS] it is actually a cancelation');
|
||||||
|
return; // not actually requesting, so ignore
|
||||||
|
}
|
||||||
|
final device = client.userDeviceKeys[client.userID]
|
||||||
|
.deviceKeys[event.content['requesting_device_id']];
|
||||||
|
if (device == null || !device.verified || device.blocked) {
|
||||||
|
print('[SSSS] Unknown / unverified devices, ignoring');
|
||||||
|
return; // nope....unknown or untrusted device
|
||||||
|
}
|
||||||
|
// alright, all seems fine...let's check if we actually have the secret they are asking for
|
||||||
|
final type = event.content['name'];
|
||||||
|
final secret = await getCached(type);
|
||||||
|
if (secret == null) {
|
||||||
|
print('[SSSS] We don\'t have the secret for ${type} ourself, ignoring');
|
||||||
|
return; // seems like we don't have this, either
|
||||||
|
}
|
||||||
|
// okay, all checks out...time to share this secret!
|
||||||
|
print('[SSSS] Replying with secret for ${type}');
|
||||||
|
await client.sendToDevice(
|
||||||
|
[device],
|
||||||
|
'm.secret.send',
|
||||||
|
{
|
||||||
|
'request_id': event.content['request_id'],
|
||||||
|
'secret': secret,
|
||||||
|
});
|
||||||
|
} else if (event.type == 'm.secret.send') {
|
||||||
|
// receiving a secret we asked for
|
||||||
|
print('[SSSS] Received shared secret...');
|
||||||
|
if (event.sender != client.userID ||
|
||||||
|
!pendingShareRequests.containsKey(event.content['request_id'])) {
|
||||||
|
print('[SSSS] Not by us or unknown request');
|
||||||
|
return; // we have no idea what we just received
|
||||||
|
}
|
||||||
|
final request = pendingShareRequests[event.content['request_id']];
|
||||||
|
// alright, as we received a known request id we know that it must have originated from a trusted source
|
||||||
|
pendingShareRequests.remove(request.requestId);
|
||||||
|
if (!(event.content['secret'] is String)) {
|
||||||
|
print('[SSSS] Secret wasn\'t a string');
|
||||||
|
return; // the secret wasn't a string....wut?
|
||||||
|
}
|
||||||
|
if (request.start.add(Duration(minutes: 15)).isBefore(DateTime.now())) {
|
||||||
|
print('[SSSS] Request is too far in the past');
|
||||||
|
return; // our request is more than 15min in the past...better not trust it anymore
|
||||||
|
}
|
||||||
|
print('[SSSS] Secret for type ${request.type} is ok, storing it');
|
||||||
|
if (client.database != null) {
|
||||||
|
final keyId = keyIdFromType(request.type);
|
||||||
|
if (keyId != null) {
|
||||||
|
await client.database.storeSSSSCache(
|
||||||
|
client.id, request.type, keyId, event.content['secret']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> keyIdsFromType(String type) {
|
||||||
|
final data = client.accountData[type];
|
||||||
|
if (data == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (data.content['encrypted'] is Map) {
|
||||||
|
final keys = Set<String>();
|
||||||
|
String maybeKey;
|
||||||
|
for (final key in data.content['encrypted'].keys) {
|
||||||
|
keys.add(key);
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String keyIdFromType(String type) {
|
||||||
|
final keys = keyIdsFromType(type);
|
||||||
|
if (keys == null || keys.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (keys.contains(defaultKeyId)) {
|
||||||
|
return defaultKeyId;
|
||||||
|
}
|
||||||
|
return keys.first;
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenSSSS open([String identifier]) {
|
||||||
|
if (identifier == null) {
|
||||||
|
identifier = defaultKeyId;
|
||||||
|
}
|
||||||
|
if (identifier == null) {
|
||||||
|
throw 'Dont know what to open';
|
||||||
|
}
|
||||||
|
final keyToOpen = keyIdFromType(identifier) ?? identifier;
|
||||||
|
if (keyToOpen == null) {
|
||||||
|
throw 'No key found to open';
|
||||||
|
}
|
||||||
|
final key = getKey(keyToOpen);
|
||||||
|
if (key == null) {
|
||||||
|
throw 'Unknown key to open';
|
||||||
|
}
|
||||||
|
return OpenSSSS(ssss: this, keyId: keyToOpen, keyData: key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ShareRequest {
|
||||||
|
final String requestId;
|
||||||
|
final String type;
|
||||||
|
final List<DeviceKeys> devices;
|
||||||
|
final DateTime start;
|
||||||
|
|
||||||
|
_ShareRequest({this.requestId, this.type, this.devices})
|
||||||
|
: start = DateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Encrypted {
|
||||||
|
final String iv;
|
||||||
|
final String ciphertext;
|
||||||
|
final String mac;
|
||||||
|
|
||||||
|
_Encrypted({this.iv, this.ciphertext, this.mac});
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DerivedKeys {
|
||||||
|
final Uint8List aesKey;
|
||||||
|
final Uint8List hmacKey;
|
||||||
|
|
||||||
|
_DerivedKeys({this.aesKey, this.hmacKey});
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PasswordInfo {
|
||||||
|
final String algorithm;
|
||||||
|
final String salt;
|
||||||
|
final int iterations;
|
||||||
|
final int bits;
|
||||||
|
|
||||||
|
_PasswordInfo({this.algorithm, this.salt, this.iterations, this.bits});
|
||||||
|
}
|
||||||
|
|
||||||
|
class OpenSSSS {
|
||||||
|
final SSSS ssss;
|
||||||
|
final String keyId;
|
||||||
|
final AccountData keyData;
|
||||||
|
OpenSSSS({this.ssss, this.keyId, this.keyData});
|
||||||
|
Uint8List privateKey;
|
||||||
|
|
||||||
|
bool get isUnlocked => privateKey != null;
|
||||||
|
|
||||||
|
void unlock({String password, String recoveryKey}) {
|
||||||
|
if (password != null) {
|
||||||
|
privateKey = SSSS.keyFromPassword(
|
||||||
|
password,
|
||||||
|
_PasswordInfo(
|
||||||
|
algorithm: keyData.content['passphrase']['algorithm'],
|
||||||
|
salt: keyData.content['passphrase']['salt'],
|
||||||
|
iterations: keyData.content['passphrase']['iterations'],
|
||||||
|
bits: keyData.content['passphrase']['bits']));
|
||||||
|
} else if (recoveryKey != null) {
|
||||||
|
privateKey = SSSS.decodeRecoveryKey(recoveryKey);
|
||||||
|
} else {
|
||||||
|
throw 'Nothing specified';
|
||||||
|
}
|
||||||
|
// verify the validity of the key
|
||||||
|
if (!ssss.checkKey(privateKey, keyData)) {
|
||||||
|
privateKey = null;
|
||||||
|
throw 'Inalid key';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> getStored(String type) async {
|
||||||
|
return await ssss.getStored(type, keyId, privateKey);
|
||||||
|
}
|
||||||
|
}
|
|
@ -132,7 +132,8 @@ class KeyVerification {
|
||||||
|
|
||||||
Future<void> start() async {
|
Future<void> start() async {
|
||||||
if (room == null) {
|
if (room == null) {
|
||||||
transactionId = randomString(512);
|
transactionId =
|
||||||
|
randomString(512) + DateTime.now().millisecondsSinceEpoch.toString();
|
||||||
}
|
}
|
||||||
await send('m.key.verification.request', {
|
await send('m.key.verification.request', {
|
||||||
'methods': VERIFICATION_METHODS,
|
'methods': VERIFICATION_METHODS,
|
||||||
|
@ -323,16 +324,31 @@ class KeyVerification {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// okay, we reached this far, so all the devices are verified!
|
// okay, we reached this far, so all the devices are verified!
|
||||||
|
var verifiedMasterKey = false;
|
||||||
|
final verifiedUserDevices = <DeviceKeys>[];
|
||||||
for (final verifyDeviceId in verifiedDevices) {
|
for (final verifyDeviceId in verifiedDevices) {
|
||||||
if (client.userDeviceKeys[userId].deviceKeys
|
if (client.userDeviceKeys[userId].deviceKeys
|
||||||
.containsKey(verifyDeviceId)) {
|
.containsKey(verifyDeviceId)) {
|
||||||
await client.userDeviceKeys[userId].deviceKeys[verifyDeviceId]
|
final key = client.userDeviceKeys[userId].deviceKeys[verifyDeviceId];
|
||||||
.setVerified(true);
|
await key.setVerified(true);
|
||||||
|
verifiedUserDevices.add(key);
|
||||||
} else if (client.userDeviceKeys[userId].crossSigningKeys
|
} else if (client.userDeviceKeys[userId].crossSigningKeys
|
||||||
.containsKey(verifyDeviceId)) {
|
.containsKey(verifyDeviceId)) {
|
||||||
await client.userDeviceKeys[userId].crossSigningKeys[verifyDeviceId]
|
final key =
|
||||||
.setVerified(true);
|
client.userDeviceKeys[userId].crossSigningKeys[verifyDeviceId];
|
||||||
// TODO: sign the other persons master key
|
await key.setVerified(true);
|
||||||
|
if (key.usage.contains('master')) {
|
||||||
|
verifiedMasterKey = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (verifiedMasterKey) {
|
||||||
|
if (userId == client.userID) {
|
||||||
|
// it was our own master key, let's request the cross signing keys
|
||||||
|
// we do it in the background, thus no await needed here
|
||||||
|
client.ssss.maybeRequestAll(verifiedUserDevices);
|
||||||
|
} else {
|
||||||
|
// it was someone elses master key, let's sign it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
37
pubspec.lock
37
pubspec.lock
|
@ -36,6 +36,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.6.0"
|
version: "1.6.0"
|
||||||
|
asn1lib:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: asn1lib
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.4"
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -43,6 +50,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.1"
|
version: "2.4.1"
|
||||||
|
base58check:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: base58check
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -134,6 +148,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.4"
|
version: "0.1.4"
|
||||||
|
clock:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: clock
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
code_builder:
|
code_builder:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -163,7 +184,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.13.9"
|
version: "0.13.9"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: crypto
|
name: crypto
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
|
@ -183,6 +204,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.6"
|
version: "1.3.6"
|
||||||
|
encrypt:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: encrypt
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.1"
|
||||||
ffi:
|
ffi:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -397,6 +425,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.3"
|
version: "1.9.3"
|
||||||
|
password_hash:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: password_hash
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -16,6 +16,10 @@ dependencies:
|
||||||
html_unescape: ^1.0.1+3
|
html_unescape: ^1.0.1+3
|
||||||
moor: ^3.0.2
|
moor: ^3.0.2
|
||||||
random_string: ^2.0.1
|
random_string: ^2.0.1
|
||||||
|
encrypt: ^4.0.1
|
||||||
|
crypto: ^2.1.4
|
||||||
|
base58check: ^1.0.1
|
||||||
|
password_hash: ^2.0.0
|
||||||
|
|
||||||
olm:
|
olm:
|
||||||
git:
|
git:
|
||||||
|
|
Loading…
Reference in a new issue