mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-07-18 02:24: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';
|
String get passwordlessRecoverySecondFactor => 'Zweiter Faktor';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecoveryNoFriendsFound =>
|
String get passwordlessRecoveryNoFriendsFound => 'Keine Freunde gefunden.';
|
||||||
'Keine vertrauenswürdigen Freunde gefunden.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecoveryActiveFriends => 'Aktive Freunde';
|
String get passwordlessRecoveryActiveFriends => 'Aktive Freunde';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecoveryActiveFriendsDesc =>
|
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
|
@override
|
||||||
String get passwordlessRecoveryInactiveFriends => 'Inaktive Freunde';
|
String get passwordlessRecoveryInactiveFriends => 'Inaktive Freunde';
|
||||||
|
|
@ -2193,7 +2192,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecoveryNeedAtLeast3 =>
|
String get passwordlessRecoveryNeedAtLeast3 =>
|
||||||
'Du brauchst mindestens 3 vertrauenswürdige Freunde.';
|
'Du brauchst mindestens 3 Freunde.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecoveryInvalidPin => 'Ungültige PIN';
|
String get passwordlessRecoveryInvalidPin => 'Ungültige PIN';
|
||||||
|
|
@ -2217,7 +2216,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String passwordlessRecoveryStatusEnabled(num count) {
|
String passwordlessRecoveryStatusEnabled(num count) {
|
||||||
return 'Aktiviert • $count vertrauenswürdige Freunde';
|
return 'Aktiviert • $count Freunde';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -2225,7 +2224,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecoveryInfoHowItWorksDesc =>
|
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
|
@override
|
||||||
String get passwordlessRecoveryInfoWhySecondFactor =>
|
String get passwordlessRecoveryInfoWhySecondFactor =>
|
||||||
|
|
@ -2233,7 +2232,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecoveryInfoWhySecondFactorDesc =>
|
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
|
@override
|
||||||
String get passwordlessRecoveryInfoGotIt => 'Verstanden';
|
String get passwordlessRecoveryInfoGotIt => 'Verstanden';
|
||||||
|
|
@ -2258,12 +2257,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String passwordlessRecoveryThresholdDesc(num count) {
|
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
|
@override
|
||||||
String get passwordlessRecoveryThresholdTitle =>
|
String get passwordlessRecoveryThresholdTitle =>
|
||||||
'Benötigte vertrauenswürdige Freunde für das Backup';
|
'Benötigte Freunde für das Backup';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String passwordlessRecoverySelectFriendsNeeded(num count) {
|
String passwordlessRecoverySelectFriendsNeeded(num count) {
|
||||||
|
|
@ -2271,12 +2270,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecoverySelectFriends =>
|
String get passwordlessRecoverySelectFriends => 'Freunde auswählen';
|
||||||
'Vertrauenswürdige Freunde auswählen';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecoveryNoFriendsSelected =>
|
String get passwordlessRecoveryNoFriendsSelected =>
|
||||||
'Noch keine vertrauenswürdigen Freunde ausgewählt';
|
'Noch keine Freunde ausgewählt';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecoverySubtitle =>
|
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
|
// 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
|
// brute-force the pin
|
||||||
List<int>? encryptedServerKeyNonce;
|
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.
|
// Checking with the server that the server data is valid and not delted throug the pin protection for example.
|
||||||
DateTime? lastServerHeartbeat;
|
DateTime? lastServerHeartbeat;
|
||||||
|
DateTime? lastContactHeartbeat;
|
||||||
List<int>? encryptedServerKey;
|
List<int>? encryptedServerKey;
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$PasswordLessRecoveryToJson(this);
|
Map<String, dynamic> toJson() => _$PasswordLessRecoveryToJson(this);
|
||||||
|
|
|
||||||
|
|
@ -255,12 +255,12 @@ PasswordLessRecovery _$PasswordLessRecoveryFromJson(
|
||||||
(json['encryptedServerKeyNonce'] as List<dynamic>?)
|
(json['encryptedServerKeyNonce'] as List<dynamic>?)
|
||||||
?.map((e) => (e as num).toInt())
|
?.map((e) => (e as num).toInt())
|
||||||
.toList()
|
.toList()
|
||||||
..encryptedServerKeyMac = (json['encryptedServerKeyMac'] as List<dynamic>?)
|
|
||||||
?.map((e) => (e as num).toInt())
|
|
||||||
.toList()
|
|
||||||
..lastServerHeartbeat = json['lastServerHeartbeat'] == null
|
..lastServerHeartbeat = json['lastServerHeartbeat'] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.parse(json['lastServerHeartbeat'] as String)
|
: DateTime.parse(json['lastServerHeartbeat'] as String)
|
||||||
|
..lastContactHeartbeat = json['lastContactHeartbeat'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['lastContactHeartbeat'] as String)
|
||||||
..encryptedServerKey = (json['encryptedServerKey'] as List<dynamic>?)
|
..encryptedServerKey = (json['encryptedServerKey'] as List<dynamic>?)
|
||||||
?.map((e) => (e as num).toInt())
|
?.map((e) => (e as num).toInt())
|
||||||
.toList();
|
.toList();
|
||||||
|
|
@ -272,7 +272,7 @@ Map<String, dynamic> _$PasswordLessRecoveryToJson(
|
||||||
'pinSeed': instance.pinSeed,
|
'pinSeed': instance.pinSeed,
|
||||||
'pinUnlockToken': instance.pinUnlockToken,
|
'pinUnlockToken': instance.pinUnlockToken,
|
||||||
'encryptedServerKeyNonce': instance.encryptedServerKeyNonce,
|
'encryptedServerKeyNonce': instance.encryptedServerKeyNonce,
|
||||||
'encryptedServerKeyMac': instance.encryptedServerKeyMac,
|
|
||||||
'lastServerHeartbeat': instance.lastServerHeartbeat?.toIso8601String(),
|
'lastServerHeartbeat': instance.lastServerHeartbeat?.toIso8601String(),
|
||||||
|
'lastContactHeartbeat': instance.lastContactHeartbeat?.toIso8601String(),
|
||||||
'encryptedServerKey': instance.encryptedServerKey,
|
'encryptedServerKey': instance.encryptedServerKey,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -435,7 +435,6 @@ class SharedSecretData extends $pb.GeneratedMessage {
|
||||||
$core.List<$core.int>? pinUnlockToken,
|
$core.List<$core.int>? pinUnlockToken,
|
||||||
$core.String? emailHint,
|
$core.String? emailHint,
|
||||||
$core.List<$core.int>? encryptedServerKeyNonce,
|
$core.List<$core.int>? encryptedServerKeyNonce,
|
||||||
$core.List<$core.int>? encryptedServerKeyMac,
|
|
||||||
}) {
|
}) {
|
||||||
final result = create();
|
final result = create();
|
||||||
if (recoveryData != null) result.recoveryData = recoveryData;
|
if (recoveryData != null) result.recoveryData = recoveryData;
|
||||||
|
|
@ -444,8 +443,6 @@ class SharedSecretData extends $pb.GeneratedMessage {
|
||||||
if (emailHint != null) result.emailHint = emailHint;
|
if (emailHint != null) result.emailHint = emailHint;
|
||||||
if (encryptedServerKeyNonce != null)
|
if (encryptedServerKeyNonce != null)
|
||||||
result.encryptedServerKeyNonce = encryptedServerKeyNonce;
|
result.encryptedServerKeyNonce = encryptedServerKeyNonce;
|
||||||
if (encryptedServerKeyMac != null)
|
|
||||||
result.encryptedServerKeyMac = encryptedServerKeyMac;
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -472,8 +469,6 @@ class SharedSecretData extends $pb.GeneratedMessage {
|
||||||
..aOS(4, _omitFieldNames ? '' : 'emailHint')
|
..aOS(4, _omitFieldNames ? '' : 'emailHint')
|
||||||
..a<$core.List<$core.int>>(
|
..a<$core.List<$core.int>>(
|
||||||
5, _omitFieldNames ? '' : 'encryptedServerKeyNonce', $pb.PbFieldType.OY)
|
5, _omitFieldNames ? '' : 'encryptedServerKeyNonce', $pb.PbFieldType.OY)
|
||||||
..a<$core.List<$core.int>>(
|
|
||||||
6, _omitFieldNames ? '' : 'encryptedServerKeyMac', $pb.PbFieldType.OY)
|
|
||||||
..hasRequiredFields = false;
|
..hasRequiredFields = false;
|
||||||
|
|
||||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
@$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);
|
$core.bool hasEncryptedServerKeyNonce() => $_has(4);
|
||||||
@$pb.TagNumber(5)
|
@$pb.TagNumber(5)
|
||||||
void clearEncryptedServerKeyNonce() => $_clearField(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 =
|
const $core.bool _omitFieldNames =
|
||||||
|
|
|
||||||
|
|
@ -151,22 +151,12 @@ const SharedSecretData$json = {
|
||||||
'10': 'encryptedServerKeyNonce',
|
'10': 'encryptedServerKeyNonce',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
'1': 'encrypted_server_key_mac',
|
|
||||||
'3': 6,
|
|
||||||
'4': 1,
|
|
||||||
'5': 12,
|
|
||||||
'9': 4,
|
|
||||||
'10': 'encryptedServerKeyMac',
|
|
||||||
'17': true
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
'8': [
|
'8': [
|
||||||
{'1': '_pin_seed'},
|
{'1': '_pin_seed'},
|
||||||
{'1': '_pin_unlock_token'},
|
{'1': '_pin_unlock_token'},
|
||||||
{'1': '_email_hint'},
|
{'1': '_email_hint'},
|
||||||
{'1': '_encrypted_server_key_nonce'},
|
{'1': '_encrypted_server_key_nonce'},
|
||||||
{'1': '_encrypted_server_key_mac'},
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -176,7 +166,5 @@ final $typed_data.Uint8List sharedSecretDataDescriptor = $convert.base64Decode(
|
||||||
'IeCghwaW5fc2VlZBgCIAEoDEgAUgdwaW5TZWVkiAEBEi0KEHBpbl91bmxvY2tfdG9rZW4YAyAB'
|
'IeCghwaW5fc2VlZBgCIAEoDEgAUgdwaW5TZWVkiAEBEi0KEHBpbl91bmxvY2tfdG9rZW4YAyAB'
|
||||||
'KAxIAVIOcGluVW5sb2NrVG9rZW6IAQESIgoKZW1haWxfaGludBgEIAEoCUgCUgllbWFpbEhpbn'
|
'KAxIAVIOcGluVW5sb2NrVG9rZW6IAQESIgoKZW1haWxfaGludBgEIAEoCUgCUgllbWFpbEhpbn'
|
||||||
'SIAQESQAoaZW5jcnlwdGVkX3NlcnZlcl9rZXlfbm9uY2UYBSABKAxIA1IXZW5jcnlwdGVkU2Vy'
|
'SIAQESQAoaZW5jcnlwdGVkX3NlcnZlcl9rZXlfbm9uY2UYBSABKAxIA1IXZW5jcnlwdGVkU2Vy'
|
||||||
'dmVyS2V5Tm9uY2WIAQESPAoYZW5jcnlwdGVkX3NlcnZlcl9rZXlfbWFjGAYgASgMSARSFWVuY3'
|
'dmVyS2V5Tm9uY2WIAQFCCwoJX3Bpbl9zZWVkQhMKEV9waW5fdW5sb2NrX3Rva2VuQg0KC19lbW'
|
||||||
'J5cHRlZFNlcnZlcktleU1hY4gBAUILCglfcGluX3NlZWRCEwoRX3Bpbl91bmxvY2tfdG9rZW5C'
|
'FpbF9oaW50Qh0KG19lbmNyeXB0ZWRfc2VydmVyX2tleV9ub25jZQ==');
|
||||||
'DQoLX2VtYWlsX2hpbnRCHQobX2VuY3J5cHRlZF9zZXJ2ZXJfa2V5X25vbmNlQhsKGV9lbmNyeX'
|
|
||||||
'B0ZWRfc2VydmVyX2tleV9tYWM=');
|
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,6 @@ message SharedSecretData {
|
||||||
|
|
||||||
optional string email_hint = 4;
|
optional string email_hint = 4;
|
||||||
optional bytes encrypted_server_key_nonce = 5;
|
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:clock/clock.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:crypto/crypto.dart' hide Hmac;
|
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'
|
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:drift/drift.dart';
|
||||||
import 'package:fixnum/fixnum.dart';
|
import 'package:fixnum/fixnum.dart';
|
||||||
import 'package:twonly/core/bridge/wrapper.dart';
|
import 'package:twonly/core/bridge/wrapper.dart';
|
||||||
|
|
@ -61,7 +59,7 @@ class PasswordlessRecoveryService {
|
||||||
await UserService.update((u) => u.passwordLessRecovery = null);
|
await UserService.update((u) => u.passwordLessRecovery = null);
|
||||||
|
|
||||||
final config = PasswordLessRecovery();
|
final config = PasswordLessRecovery();
|
||||||
final chacha20 = FlutterChacha20.poly1305Aead();
|
final xchacha20 = Xchacha20.poly1305Aead();
|
||||||
|
|
||||||
// 2. If enabled, handle the second factor and create serverKey
|
// 2. If enabled, handle the second factor and create serverKey
|
||||||
|
|
||||||
|
|
@ -99,7 +97,10 @@ class PasswordlessRecoveryService {
|
||||||
|
|
||||||
// Brute-force protection for the user's pin:
|
// Brute-force protection for the user's pin:
|
||||||
// - Server: Does not know the seed.
|
// - 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(
|
final pinProtectionKey = await Hmac.sha256().calculateMac(
|
||||||
Uint8List.fromList(utf8.encode(secondFactorValue)),
|
Uint8List.fromList(utf8.encode(secondFactorValue)),
|
||||||
secretKey: SecretKey(config.pinSeed!),
|
secretKey: SecretKey(config.pinSeed!),
|
||||||
|
|
@ -118,21 +119,24 @@ class PasswordlessRecoveryService {
|
||||||
// colaberate, they additional need the serverKey to decrypt the user's key.
|
// colaberate, they additional need the serverKey to decrypt the user's key.
|
||||||
serverKey = getRandomUint8List(32);
|
serverKey = getRandomUint8List(32);
|
||||||
|
|
||||||
final secretBox = await chacha20.encrypt(
|
final secretBox = await xchacha20.encrypt(
|
||||||
serverKey,
|
serverKey,
|
||||||
secretKey: secondFactorEncryptedServerKeyKey,
|
secretKey: secondFactorEncryptedServerKeyKey,
|
||||||
nonce: chacha20.newNonce(),
|
nonce: xchacha20.newNonce(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// This is send to the server and then deleted localy
|
// The server only gets the encrypted server key and the mac. Because the server does not know the nonce (192-bit
|
||||||
encryptedServerKey = Uint8List.fromList(secretBox.cipherText);
|
// 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
|
config
|
||||||
..encryptedServerKeyNonce = secretBox.nonce
|
..encryptedServerKeyNonce = secretBox.nonce
|
||||||
..encryptedServerKey = encryptedServerKey
|
..encryptedServerKey = encryptedServerKey;
|
||||||
..encryptedServerKeyMac = secretBox.mac.bytes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Using shamir's secret to generate the shares for the users.
|
// 3. Using shamir's secret to generate the shares for the users.
|
||||||
|
|
@ -147,10 +151,10 @@ class PasswordlessRecoveryService {
|
||||||
if (serverKey != null) {
|
if (serverKey != null) {
|
||||||
// Second factor was enabled, so encrypt the recoveryData using the serverKey.
|
// Second factor was enabled, so encrypt the recoveryData using the serverKey.
|
||||||
|
|
||||||
final secretBox = await chacha20.encrypt(
|
final secretBox = await xchacha20.encrypt(
|
||||||
recoveryData,
|
recoveryData,
|
||||||
secretKey: SecretKey(serverKey),
|
secretKey: SecretKey(serverKey),
|
||||||
nonce: chacha20.newNonce(),
|
nonce: xchacha20.newNonce(),
|
||||||
);
|
);
|
||||||
|
|
||||||
recoveryData = EncryptedEnvelope(
|
recoveryData = EncryptedEnvelope(
|
||||||
|
|
@ -165,7 +169,6 @@ class PasswordlessRecoveryService {
|
||||||
pinSeed: config.pinSeed,
|
pinSeed: config.pinSeed,
|
||||||
pinUnlockToken: config.pinUnlockToken,
|
pinUnlockToken: config.pinUnlockToken,
|
||||||
emailHint: emailHint,
|
emailHint: emailHint,
|
||||||
encryptedServerKeyMac: config.encryptedServerKeyMac,
|
|
||||||
encryptedServerKeyNonce: config.encryptedServerKeyNonce,
|
encryptedServerKeyNonce: config.encryptedServerKeyNonce,
|
||||||
).writeToBuffer();
|
).writeToBuffer();
|
||||||
|
|
||||||
|
|
@ -224,15 +227,19 @@ class PasswordlessRecoveryService {
|
||||||
pinProtectionKey.bytes,
|
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(
|
final secretBox = SecretBox(
|
||||||
config.encryptedServerKey!,
|
cipherText,
|
||||||
nonce: config.encryptedServerKeyNonce!,
|
nonce: config.encryptedServerKeyNonce!,
|
||||||
mac: Mac(config.encryptedServerKeyMac!),
|
mac: Mac(macBytes),
|
||||||
);
|
);
|
||||||
|
|
||||||
await chacha20.decrypt(
|
await xchacha20.decrypt(
|
||||||
secretBox,
|
secretBox,
|
||||||
secretKey: secondFactorEncryptedServerKeyKey,
|
secretKey: secondFactorEncryptedServerKeyKey,
|
||||||
);
|
);
|
||||||
|
|
@ -251,7 +258,7 @@ class PasswordlessRecoveryService {
|
||||||
final lastHeartbeat = config.lastServerHeartbeat;
|
final lastHeartbeat = config.lastServerHeartbeat;
|
||||||
final isOlderThanAMonth =
|
final isOlderThanAMonth =
|
||||||
lastHeartbeat != null &&
|
lastHeartbeat != null &&
|
||||||
clock.now().difference(lastHeartbeat).inDays > 30;
|
clock.now().difference(lastHeartbeat).inDays > 20;
|
||||||
|
|
||||||
if ((lastHeartbeat == null || isOlderThanAMonth) &&
|
if ((lastHeartbeat == null || isOlderThanAMonth) &&
|
||||||
config.encryptedServerKey != null) {
|
config.encryptedServerKey != null) {
|
||||||
|
|
@ -267,39 +274,43 @@ class PasswordlessRecoveryService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all contacts where recoveryLastHeartbeat is NULL. Then for each contacts send
|
final lastContactHeartbeat = config.lastContactHeartbeat;
|
||||||
final pendingShares =
|
final isContactHeartbeatOlderThan24h =
|
||||||
await (twonlyDB.select(twonlyDB.contacts)..where(
|
lastContactHeartbeat == null ||
|
||||||
(t) =>
|
clock.now().difference(lastContactHeartbeat).inHours >= 24;
|
||||||
t.recoveryIsTrustedFriend.equals(true) &
|
|
||||||
t.recoveryLastHeartbeat.isNull() &
|
|
||||||
t.recoverySecretShare.isNotNull(),
|
|
||||||
))
|
|
||||||
.get();
|
|
||||||
|
|
||||||
for (final contact in pendingShares) {
|
if (isContactHeartbeatOlderThan24h) {
|
||||||
try {
|
// Get all contacts where recoveryLastHeartbeat is NULL. Then for each contacts send
|
||||||
await sendCipherText(
|
final pendingShares =
|
||||||
contact.userId,
|
await (twonlyDB.select(twonlyDB.contacts)..where(
|
||||||
pb.EncryptedContent(
|
(t) =>
|
||||||
passwordlessRecovery: pb.EncryptedContent_PasswordLessRecovery(
|
t.recoveryIsTrustedFriend.equals(true) &
|
||||||
recoverySecretShare: contact.recoverySecretShare,
|
t.recoveryLastHeartbeat.isNull() &
|
||||||
delete: false,
|
t.recoverySecretShare.isNotNull(),
|
||||||
|
))
|
||||||
|
.get();
|
||||||
|
|
||||||
|
for (final contact in pendingShares) {
|
||||||
|
try {
|
||||||
|
await sendCipherText(
|
||||||
|
contact.userId,
|
||||||
|
pb.EncryptedContent(
|
||||||
|
passwordlessRecovery: pb.EncryptedContent_PasswordLessRecovery(
|
||||||
|
recoverySecretShare: contact.recoverySecretShare,
|
||||||
|
delete: false,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
} catch (e) {
|
||||||
|
Log.error(
|
||||||
await twonlyDB.contactsDao.updateContact(
|
'Failed to send PasswordLessRecovery share to contact ${contact.userId}: $e',
|
||||||
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();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue