mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-07-18 01:14:07 +00:00
store the mac on the server
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
This commit is contained in:
parent
f8a2240992
commit
ca77cc4902
8 changed files with 80 additions and 99 deletions
|
|
@ -2159,15 +2159,14 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get passwordlessRecoverySecondFactor => 'Zweiter Faktor';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryNoFriendsFound =>
|
||||
'Keine vertrauenswürdigen Freunde gefunden.';
|
||||
String get passwordlessRecoveryNoFriendsFound => 'Keine Freunde gefunden.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryActiveFriends => 'Aktive Freunde';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryActiveFriendsDesc =>
|
||||
'Diese vertrauenswürdigen Freunde nutzen twonly aktiv und können dir wahrscheinlich helfen, dein Konto wiederherzustellen.';
|
||||
'Diese Freunde nutzen twonly aktiv und können dir wahrscheinlich helfen, dein Konto wiederherzustellen.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryInactiveFriends => 'Inaktive Freunde';
|
||||
|
|
@ -2193,7 +2192,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get passwordlessRecoveryNeedAtLeast3 =>
|
||||
'Du brauchst mindestens 3 vertrauenswürdige Freunde.';
|
||||
'Du brauchst mindestens 3 Freunde.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryInvalidPin => 'Ungültige PIN';
|
||||
|
|
@ -2217,7 +2216,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String passwordlessRecoveryStatusEnabled(num count) {
|
||||
return 'Aktiviert • $count vertrauenswürdige Freunde';
|
||||
return 'Aktiviert • $count Freunde';
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -2225,7 +2224,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get passwordlessRecoveryInfoHowItWorksDesc =>
|
||||
'Da twonly ohne zentrale Benutzerkonten oder Telefonnummern funktioniert, um deine Privatsphäre zu maximieren, setzen wir auf einen dezentralen Wiederherstellungsmechanismus. Mit Shamir\'s Secret Sharing wird deine kryptografische Identität in unabhängige Teile aufgeteilt und an deine vertrauenswürdigen Freunde verteilt. Um den Zugriff wiederherzustellen, muss ein vorher festgelegter Schwellenwert dieser Freunde ihre Teile kombinieren.';
|
||||
'Da twonly ohne zentrale Benutzerkonten oder Telefonnummern funktioniert, um deine Privatsphäre zu maximieren, setzen wir auf einen dezentralen Wiederherstellungsmechanismus. Mit Shamir\'s Secret Sharing wird deine kryptografische Identität in unabhängige Teile aufgeteilt und an deine Freunde verteilt. Um den Zugriff wiederherzustellen, muss ein vorher festgelegter Schwellenwert dieser Freunde ihre Teile kombinieren.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryInfoWhySecondFactor =>
|
||||
|
|
@ -2233,7 +2232,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get passwordlessRecoveryInfoWhySecondFactorDesc =>
|
||||
'Der zweite Faktor (E-Mail oder PIN) dient als wichtiger kryptografischer Schutz vor bösartiger Absprache. Wenn sich deine vertrauenswürdigen Freunde hinter deinem Rücken absprechen würden, könnten sie deine Identität ohne den Schlüssel für den zweiten Faktor trotzdem nicht entschlüsseln.';
|
||||
'Der zweite Faktor (E-Mail oder PIN) dient als wichtiger kryptografischer Schutz vor bösartiger Absprache. Wenn sich deine Freunde hinter deinem Rücken absprechen würden, könnten sie deine Identität ohne den Schlüssel für den zweiten Faktor trotzdem nicht entschlüsseln.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryInfoGotIt => 'Verstanden';
|
||||
|
|
@ -2258,12 +2257,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String passwordlessRecoveryThresholdDesc(num count) {
|
||||
return 'Um dein Konto wiederherzustellen, brauchst du $count deiner ausgewählten vertrauenswürdigen Freunde.';
|
||||
return 'Um dein Konto wiederherzustellen, brauchst du $count deiner ausgewählten Freunde.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryThresholdTitle =>
|
||||
'Benötigte vertrauenswürdige Freunde für das Backup';
|
||||
'Benötigte Freunde für das Backup';
|
||||
|
||||
@override
|
||||
String passwordlessRecoverySelectFriendsNeeded(num count) {
|
||||
|
|
@ -2271,12 +2270,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
}
|
||||
|
||||
@override
|
||||
String get passwordlessRecoverySelectFriends =>
|
||||
'Vertrauenswürdige Freunde auswählen';
|
||||
String get passwordlessRecoverySelectFriends => 'Freunde auswählen';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryNoFriendsSelected =>
|
||||
'Noch keine vertrauenswürdigen Freunde ausgewählt';
|
||||
'Noch keine Freunde ausgewählt';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoverySubtitle =>
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit efb09c059d5885ec58e78189988ecbb52f00628b
|
||||
Subproject commit 4d8e1373bf3fea617157a86791be7c65edd879d3
|
||||
|
|
@ -225,11 +225,11 @@ class PasswordLessRecovery {
|
|||
// Stored not on the server, so the server is unable to link a email to a user until the actuall recovery or can
|
||||
// brute-force the pin
|
||||
List<int>? encryptedServerKeyNonce;
|
||||
List<int>? encryptedServerKeyMac;
|
||||
// --->
|
||||
|
||||
// Checking with the server that the server data is valid and not delted throug the pin protection for example.
|
||||
DateTime? lastServerHeartbeat;
|
||||
DateTime? lastContactHeartbeat;
|
||||
List<int>? encryptedServerKey;
|
||||
|
||||
Map<String, dynamic> toJson() => _$PasswordLessRecoveryToJson(this);
|
||||
|
|
|
|||
|
|
@ -255,12 +255,12 @@ PasswordLessRecovery _$PasswordLessRecoveryFromJson(
|
|||
(json['encryptedServerKeyNonce'] as List<dynamic>?)
|
||||
?.map((e) => (e as num).toInt())
|
||||
.toList()
|
||||
..encryptedServerKeyMac = (json['encryptedServerKeyMac'] as List<dynamic>?)
|
||||
?.map((e) => (e as num).toInt())
|
||||
.toList()
|
||||
..lastServerHeartbeat = json['lastServerHeartbeat'] == null
|
||||
? null
|
||||
: DateTime.parse(json['lastServerHeartbeat'] as String)
|
||||
..lastContactHeartbeat = json['lastContactHeartbeat'] == null
|
||||
? null
|
||||
: DateTime.parse(json['lastContactHeartbeat'] as String)
|
||||
..encryptedServerKey = (json['encryptedServerKey'] as List<dynamic>?)
|
||||
?.map((e) => (e as num).toInt())
|
||||
.toList();
|
||||
|
|
@ -272,7 +272,7 @@ Map<String, dynamic> _$PasswordLessRecoveryToJson(
|
|||
'pinSeed': instance.pinSeed,
|
||||
'pinUnlockToken': instance.pinUnlockToken,
|
||||
'encryptedServerKeyNonce': instance.encryptedServerKeyNonce,
|
||||
'encryptedServerKeyMac': instance.encryptedServerKeyMac,
|
||||
'lastServerHeartbeat': instance.lastServerHeartbeat?.toIso8601String(),
|
||||
'lastContactHeartbeat': instance.lastContactHeartbeat?.toIso8601String(),
|
||||
'encryptedServerKey': instance.encryptedServerKey,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -435,7 +435,6 @@ class SharedSecretData extends $pb.GeneratedMessage {
|
|||
$core.List<$core.int>? pinUnlockToken,
|
||||
$core.String? emailHint,
|
||||
$core.List<$core.int>? encryptedServerKeyNonce,
|
||||
$core.List<$core.int>? encryptedServerKeyMac,
|
||||
}) {
|
||||
final result = create();
|
||||
if (recoveryData != null) result.recoveryData = recoveryData;
|
||||
|
|
@ -444,8 +443,6 @@ class SharedSecretData extends $pb.GeneratedMessage {
|
|||
if (emailHint != null) result.emailHint = emailHint;
|
||||
if (encryptedServerKeyNonce != null)
|
||||
result.encryptedServerKeyNonce = encryptedServerKeyNonce;
|
||||
if (encryptedServerKeyMac != null)
|
||||
result.encryptedServerKeyMac = encryptedServerKeyMac;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -472,8 +469,6 @@ class SharedSecretData extends $pb.GeneratedMessage {
|
|||
..aOS(4, _omitFieldNames ? '' : 'emailHint')
|
||||
..a<$core.List<$core.int>>(
|
||||
5, _omitFieldNames ? '' : 'encryptedServerKeyNonce', $pb.PbFieldType.OY)
|
||||
..a<$core.List<$core.int>>(
|
||||
6, _omitFieldNames ? '' : 'encryptedServerKeyMac', $pb.PbFieldType.OY)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
|
|
@ -541,16 +536,6 @@ class SharedSecretData extends $pb.GeneratedMessage {
|
|||
$core.bool hasEncryptedServerKeyNonce() => $_has(4);
|
||||
@$pb.TagNumber(5)
|
||||
void clearEncryptedServerKeyNonce() => $_clearField(5);
|
||||
|
||||
@$pb.TagNumber(6)
|
||||
$core.List<$core.int> get encryptedServerKeyMac => $_getN(5);
|
||||
@$pb.TagNumber(6)
|
||||
set encryptedServerKeyMac($core.List<$core.int> value) =>
|
||||
$_setBytes(5, value);
|
||||
@$pb.TagNumber(6)
|
||||
$core.bool hasEncryptedServerKeyMac() => $_has(5);
|
||||
@$pb.TagNumber(6)
|
||||
void clearEncryptedServerKeyMac() => $_clearField(6);
|
||||
}
|
||||
|
||||
const $core.bool _omitFieldNames =
|
||||
|
|
|
|||
|
|
@ -151,22 +151,12 @@ const SharedSecretData$json = {
|
|||
'10': 'encryptedServerKeyNonce',
|
||||
'17': true
|
||||
},
|
||||
{
|
||||
'1': 'encrypted_server_key_mac',
|
||||
'3': 6,
|
||||
'4': 1,
|
||||
'5': 12,
|
||||
'9': 4,
|
||||
'10': 'encryptedServerKeyMac',
|
||||
'17': true
|
||||
},
|
||||
],
|
||||
'8': [
|
||||
{'1': '_pin_seed'},
|
||||
{'1': '_pin_unlock_token'},
|
||||
{'1': '_email_hint'},
|
||||
{'1': '_encrypted_server_key_nonce'},
|
||||
{'1': '_encrypted_server_key_mac'},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
@ -176,7 +166,5 @@ final $typed_data.Uint8List sharedSecretDataDescriptor = $convert.base64Decode(
|
|||
'IeCghwaW5fc2VlZBgCIAEoDEgAUgdwaW5TZWVkiAEBEi0KEHBpbl91bmxvY2tfdG9rZW4YAyAB'
|
||||
'KAxIAVIOcGluVW5sb2NrVG9rZW6IAQESIgoKZW1haWxfaGludBgEIAEoCUgCUgllbWFpbEhpbn'
|
||||
'SIAQESQAoaZW5jcnlwdGVkX3NlcnZlcl9rZXlfbm9uY2UYBSABKAxIA1IXZW5jcnlwdGVkU2Vy'
|
||||
'dmVyS2V5Tm9uY2WIAQESPAoYZW5jcnlwdGVkX3NlcnZlcl9rZXlfbWFjGAYgASgMSARSFWVuY3'
|
||||
'J5cHRlZFNlcnZlcktleU1hY4gBAUILCglfcGluX3NlZWRCEwoRX3Bpbl91bmxvY2tfdG9rZW5C'
|
||||
'DQoLX2VtYWlsX2hpbnRCHQobX2VuY3J5cHRlZF9zZXJ2ZXJfa2V5X25vbmNlQhsKGV9lbmNyeX'
|
||||
'B0ZWRfc2VydmVyX2tleV9tYWM=');
|
||||
'dmVyS2V5Tm9uY2WIAQFCCwoJX3Bpbl9zZWVkQhMKEV9waW5fdW5sb2NrX3Rva2VuQg0KC19lbW'
|
||||
'FpbF9oaW50Qh0KG19lbmNyeXB0ZWRfc2VydmVyX2tleV9ub25jZQ==');
|
||||
|
|
|
|||
|
|
@ -63,7 +63,6 @@ message SharedSecretData {
|
|||
|
||||
optional string email_hint = 4;
|
||||
optional bytes encrypted_server_key_nonce = 5;
|
||||
optional bytes encrypted_server_key_mac = 6;
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,10 +4,8 @@ import 'dart:convert' show utf8;
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:crypto/crypto.dart' hide Hmac;
|
||||
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'
|
||||
show FlutterChacha20;
|
||||
import 'package:cryptography_plus/cryptography_plus.dart'
|
||||
show Hmac, Mac, SecretBox, SecretKey;
|
||||
show Hmac, Mac, SecretBox, SecretKey, Xchacha20;
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:twonly/core/bridge/wrapper.dart';
|
||||
|
|
@ -61,7 +59,7 @@ class PasswordlessRecoveryService {
|
|||
await UserService.update((u) => u.passwordLessRecovery = null);
|
||||
|
||||
final config = PasswordLessRecovery();
|
||||
final chacha20 = FlutterChacha20.poly1305Aead();
|
||||
final xchacha20 = Xchacha20.poly1305Aead();
|
||||
|
||||
// 2. If enabled, handle the second factor and create serverKey
|
||||
|
||||
|
|
@ -99,7 +97,10 @@ class PasswordlessRecoveryService {
|
|||
|
||||
// Brute-force protection for the user's pin:
|
||||
// - Server: Does not know the seed.
|
||||
// - Trusted friends: Can only check the result X times before the server deletes the key.
|
||||
// - Trusted friends:
|
||||
// Can only check the result X times before the server deletes the key. As they do not have
|
||||
// the mac and the cypher text they are unable to brute-force the pin localy. And the server only allows 10
|
||||
// tries.
|
||||
final pinProtectionKey = await Hmac.sha256().calculateMac(
|
||||
Uint8List.fromList(utf8.encode(secondFactorValue)),
|
||||
secretKey: SecretKey(config.pinSeed!),
|
||||
|
|
@ -118,21 +119,24 @@ class PasswordlessRecoveryService {
|
|||
// colaberate, they additional need the serverKey to decrypt the user's key.
|
||||
serverKey = getRandomUint8List(32);
|
||||
|
||||
final secretBox = await chacha20.encrypt(
|
||||
final secretBox = await xchacha20.encrypt(
|
||||
serverKey,
|
||||
secretKey: secondFactorEncryptedServerKeyKey,
|
||||
nonce: chacha20.newNonce(),
|
||||
nonce: xchacha20.newNonce(),
|
||||
);
|
||||
|
||||
// This is send to the server and then deleted localy
|
||||
encryptedServerKey = Uint8List.fromList(secretBox.cipherText);
|
||||
// The server only gets the encrypted server key and the mac. Because the server does not know the nonce (192-bit
|
||||
// because of XChaCha), he is unable to decrypt the server key without the help of the trusted friends. This
|
||||
// ensures that the server never learns the users orginal pin, as he is missing the pin_seed and also unable to
|
||||
// brute-force the email of the user as he does not have the nonce.
|
||||
encryptedServerKey = Uint8List.fromList([
|
||||
...secretBox.cipherText,
|
||||
...secretBox.mac.bytes,
|
||||
]);
|
||||
|
||||
// Because the server does not know the nonce and the mac, he is unable to link a email to the user and also
|
||||
// unable to brute-force the pin.
|
||||
config
|
||||
..encryptedServerKeyNonce = secretBox.nonce
|
||||
..encryptedServerKey = encryptedServerKey
|
||||
..encryptedServerKeyMac = secretBox.mac.bytes;
|
||||
..encryptedServerKey = encryptedServerKey;
|
||||
}
|
||||
|
||||
// 3. Using shamir's secret to generate the shares for the users.
|
||||
|
|
@ -147,10 +151,10 @@ class PasswordlessRecoveryService {
|
|||
if (serverKey != null) {
|
||||
// Second factor was enabled, so encrypt the recoveryData using the serverKey.
|
||||
|
||||
final secretBox = await chacha20.encrypt(
|
||||
final secretBox = await xchacha20.encrypt(
|
||||
recoveryData,
|
||||
secretKey: SecretKey(serverKey),
|
||||
nonce: chacha20.newNonce(),
|
||||
nonce: xchacha20.newNonce(),
|
||||
);
|
||||
|
||||
recoveryData = EncryptedEnvelope(
|
||||
|
|
@ -165,7 +169,6 @@ class PasswordlessRecoveryService {
|
|||
pinSeed: config.pinSeed,
|
||||
pinUnlockToken: config.pinUnlockToken,
|
||||
emailHint: emailHint,
|
||||
encryptedServerKeyMac: config.encryptedServerKeyMac,
|
||||
encryptedServerKeyNonce: config.encryptedServerKeyNonce,
|
||||
).writeToBuffer();
|
||||
|
||||
|
|
@ -224,15 +227,19 @@ class PasswordlessRecoveryService {
|
|||
pinProtectionKey.bytes,
|
||||
);
|
||||
|
||||
final chacha20 = FlutterChacha20.poly1305Aead();
|
||||
final xchacha20 = Xchacha20.poly1305Aead();
|
||||
|
||||
final combined = config.encryptedServerKey!;
|
||||
final cipherText = combined.sublist(0, combined.length - 16);
|
||||
final macBytes = combined.sublist(combined.length - 16);
|
||||
|
||||
final secretBox = SecretBox(
|
||||
config.encryptedServerKey!,
|
||||
cipherText,
|
||||
nonce: config.encryptedServerKeyNonce!,
|
||||
mac: Mac(config.encryptedServerKeyMac!),
|
||||
mac: Mac(macBytes),
|
||||
);
|
||||
|
||||
await chacha20.decrypt(
|
||||
await xchacha20.decrypt(
|
||||
secretBox,
|
||||
secretKey: secondFactorEncryptedServerKeyKey,
|
||||
);
|
||||
|
|
@ -251,7 +258,7 @@ class PasswordlessRecoveryService {
|
|||
final lastHeartbeat = config.lastServerHeartbeat;
|
||||
final isOlderThanAMonth =
|
||||
lastHeartbeat != null &&
|
||||
clock.now().difference(lastHeartbeat).inDays > 30;
|
||||
clock.now().difference(lastHeartbeat).inDays > 20;
|
||||
|
||||
if ((lastHeartbeat == null || isOlderThanAMonth) &&
|
||||
config.encryptedServerKey != null) {
|
||||
|
|
@ -267,6 +274,12 @@ class PasswordlessRecoveryService {
|
|||
}
|
||||
}
|
||||
|
||||
final lastContactHeartbeat = config.lastContactHeartbeat;
|
||||
final isContactHeartbeatOlderThan24h =
|
||||
lastContactHeartbeat == null ||
|
||||
clock.now().difference(lastContactHeartbeat).inHours >= 24;
|
||||
|
||||
if (isContactHeartbeatOlderThan24h) {
|
||||
// Get all contacts where recoveryLastHeartbeat is NULL. Then for each contacts send
|
||||
final pendingShares =
|
||||
await (twonlyDB.select(twonlyDB.contacts)..where(
|
||||
|
|
@ -288,19 +301,17 @@ class PasswordlessRecoveryService {
|
|||
),
|
||||
),
|
||||
);
|
||||
|
||||
await twonlyDB.contactsDao.updateContact(
|
||||
contact.userId,
|
||||
ContactsCompanion(
|
||||
recoveryLastHeartbeat: Value(clock.now()),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error(
|
||||
'Failed to send PasswordLessRecovery share to contact ${contact.userId}: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await UserService.update((u) {
|
||||
u.passwordLessRecovery?.lastContactHeartbeat = clock.now();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Send heartbeat to the friends I am a trusted friend.
|
||||
|
|
|
|||
Loading…
Reference in a new issue