mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-07-18 02:24:07 +00:00
poc for recovery works
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
7ea311236d
commit
991376e1d9
21 changed files with 726 additions and 102 deletions
|
|
@ -191,7 +191,8 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
||||||
_proofOfWork = (null, disabled);
|
_proofOfWork = (null, disabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onboardingState.hasStartedPasswordlessRecovery) {
|
if (onboardingState.hasStartedPasswordlessRecovery ||
|
||||||
|
onboardingState.emailRecoveryRequested) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
routerProvider.push(Routes.recoverPasswordless);
|
routerProvider.push(Routes.recoverPasswordless);
|
||||||
});
|
});
|
||||||
|
|
@ -199,7 +200,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
||||||
}
|
}
|
||||||
|
|
||||||
await PasswordlessRecoveryService.handleRecoveryLink(
|
await PasswordlessRecoveryService.handleRecoveryLink(
|
||||||
'https://me.twonly.eu/r/#d0e204fa-3d1e-4e53-a6a5-53aadf1e2ee5/oHeAEPp_CivU3QO67L0AkSlM94E5klbP8XDLbNgsGmA',
|
'https://me.twonly.eu/r/#7fdb8f08-0927-4e44-8761-038993414e48/1p7SKEzpxE3wSW9FQw60EUI4OpSW2U4EskdXLw8xg48',
|
||||||
);
|
);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,12 @@ class RustKeyManager {
|
||||||
static Future<PlatformInt64?> getUserId() => RustLib.instance.api
|
static Future<PlatformInt64?> getUserId() => RustLib.instance.api
|
||||||
.crateBridgeWrapperKeyManagerRustKeyManagerGetUserId();
|
.crateBridgeWrapperKeyManagerRustKeyManagerGetUserId();
|
||||||
|
|
||||||
|
static Future<void> importSerialized({required List<int> serializedBytes}) =>
|
||||||
|
RustLib.instance.api
|
||||||
|
.crateBridgeWrapperKeyManagerRustKeyManagerImportSerialized(
|
||||||
|
serializedBytes: serializedBytes,
|
||||||
|
);
|
||||||
|
|
||||||
static Future<void> importSignalIdentity({
|
static Future<void> importSignalIdentity({
|
||||||
required List<int> identityKeyPairStructure,
|
required List<int> identityKeyPairStructure,
|
||||||
required PlatformInt64 registrationId,
|
required PlatformInt64 registrationId,
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
|
||||||
String get codegenVersion => '2.12.0';
|
String get codegenVersion => '2.12.0';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get rustContentHash => -508562557;
|
int get rustContentHash => 340781866;
|
||||||
|
|
||||||
static const kDefaultExternalLibraryLoaderConfig =
|
static const kDefaultExternalLibraryLoaderConfig =
|
||||||
ExternalLibraryLoaderConfig(
|
ExternalLibraryLoaderConfig(
|
||||||
|
|
@ -212,6 +212,10 @@ abstract class RustLibApi extends BaseApi {
|
||||||
|
|
||||||
Future<PlatformInt64?> crateBridgeWrapperKeyManagerRustKeyManagerGetUserId();
|
Future<PlatformInt64?> crateBridgeWrapperKeyManagerRustKeyManagerGetUserId();
|
||||||
|
|
||||||
|
Future<void> crateBridgeWrapperKeyManagerRustKeyManagerImportSerialized({
|
||||||
|
required List<int> serializedBytes,
|
||||||
|
});
|
||||||
|
|
||||||
Future<void> crateBridgeWrapperKeyManagerRustKeyManagerImportSignalIdentity({
|
Future<void> crateBridgeWrapperKeyManagerRustKeyManagerImportSignalIdentity({
|
||||||
required List<int> identityKeyPairStructure,
|
required List<int> identityKeyPairStructure,
|
||||||
required PlatformInt64 registrationId,
|
required PlatformInt64 registrationId,
|
||||||
|
|
@ -1234,6 +1238,41 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
argNames: [],
|
argNames: [],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> crateBridgeWrapperKeyManagerRustKeyManagerImportSerialized({
|
||||||
|
required List<int> serializedBytes,
|
||||||
|
}) {
|
||||||
|
return handler.executeNormal(
|
||||||
|
NormalTask(
|
||||||
|
callFfi: (port_) {
|
||||||
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
|
sse_encode_list_prim_u_8_loose(serializedBytes, serializer);
|
||||||
|
pdeCallFfi(
|
||||||
|
generalizedFrbRustBinding,
|
||||||
|
serializer,
|
||||||
|
funcId: 21,
|
||||||
|
port: port_,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
codec: SseCodec(
|
||||||
|
decodeSuccessData: sse_decode_unit,
|
||||||
|
decodeErrorData: sse_decode_AnyhowException,
|
||||||
|
),
|
||||||
|
constMeta:
|
||||||
|
kCrateBridgeWrapperKeyManagerRustKeyManagerImportSerializedConstMeta,
|
||||||
|
argValues: [serializedBytes],
|
||||||
|
apiImpl: this,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskConstMeta
|
||||||
|
get kCrateBridgeWrapperKeyManagerRustKeyManagerImportSerializedConstMeta =>
|
||||||
|
const TaskConstMeta(
|
||||||
|
debugName: "rust_key_manager_import_serialized",
|
||||||
|
argNames: ["serializedBytes"],
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> crateBridgeWrapperKeyManagerRustKeyManagerImportSignalIdentity({
|
Future<void> crateBridgeWrapperKeyManagerRustKeyManagerImportSignalIdentity({
|
||||||
required List<int> identityKeyPairStructure,
|
required List<int> identityKeyPairStructure,
|
||||||
|
|
@ -1253,7 +1292,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 21,
|
funcId: 22,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -1297,7 +1336,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 22,
|
funcId: 23,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -1330,7 +1369,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 23,
|
funcId: 24,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -1362,7 +1401,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 24,
|
funcId: 25,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -1397,7 +1436,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 25,
|
funcId: 26,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -1429,7 +1468,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 26,
|
funcId: 27,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -1464,7 +1503,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 27,
|
funcId: 28,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -1501,7 +1540,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 28,
|
funcId: 29,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -1540,7 +1579,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 29,
|
funcId: 30,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -1575,7 +1614,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 30,
|
funcId: 31,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -1611,7 +1650,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 31,
|
funcId: 32,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -1648,7 +1687,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 32,
|
funcId: 33,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -1686,7 +1725,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 33,
|
funcId: 34,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -1724,7 +1763,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 34,
|
funcId: 35,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -1762,7 +1801,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 35,
|
funcId: 36,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -1801,7 +1840,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 36,
|
funcId: 37,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -1840,7 +1879,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 37,
|
funcId: 38,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -1885,7 +1924,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 38,
|
funcId: 39,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -1937,7 +1976,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 39,
|
funcId: 40,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -1978,7 +2017,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 40,
|
funcId: 41,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -2016,7 +2055,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 41,
|
funcId: 42,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -2054,7 +2093,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 42,
|
funcId: 43,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -2092,7 +2131,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 43,
|
funcId: 44,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -2130,7 +2169,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 44,
|
funcId: 45,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -2172,7 +2211,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 45,
|
funcId: 46,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -2212,7 +2251,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 46,
|
funcId: 47,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4032,6 +4032,18 @@ abstract class AppLocalizations {
|
||||||
/// **'Network error, please ensure you have internet'**
|
/// **'Network error, please ensure you have internet'**
|
||||||
String get passwordlessRecoveryNetworkError;
|
String get passwordlessRecoveryNetworkError;
|
||||||
|
|
||||||
|
/// No description provided for @passwordlessRecoveryInvalidEmail.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'The email address is invalid.'**
|
||||||
|
String get passwordlessRecoveryInvalidEmail;
|
||||||
|
|
||||||
|
/// No description provided for @passwordlessRecoveryResendEmail.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Resend recovery email'**
|
||||||
|
String get passwordlessRecoveryResendEmail;
|
||||||
|
|
||||||
/// No description provided for @passwordlessRecoveryHelpAFriend.
|
/// No description provided for @passwordlessRecoveryHelpAFriend.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
|
||||||
|
|
@ -2325,6 +2325,13 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get passwordlessRecoveryNetworkError =>
|
String get passwordlessRecoveryNetworkError =>
|
||||||
'Netzwerkfehler, bitte stelle sicher, dass du Internet hast';
|
'Netzwerkfehler, bitte stelle sicher, dass du Internet hast';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get passwordlessRecoveryInvalidEmail =>
|
||||||
|
'Die E-Mail-Adresse ist ungültig.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get passwordlessRecoveryResendEmail => 'E-Mail erneut senden';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecoveryHelpAFriend => 'Einem Freund helfen';
|
String get passwordlessRecoveryHelpAFriend => 'Einem Freund helfen';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2306,6 +2306,13 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get passwordlessRecoveryNetworkError =>
|
String get passwordlessRecoveryNetworkError =>
|
||||||
'Network error, please ensure you have internet';
|
'Network error, please ensure you have internet';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get passwordlessRecoveryInvalidEmail =>
|
||||||
|
'The email address is invalid.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get passwordlessRecoveryResendEmail => 'Resend recovery email';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get passwordlessRecoveryHelpAFriend => 'Help a Friend';
|
String get passwordlessRecoveryHelpAFriend => 'Help a Friend';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit bd641ce3a3f3b9c709dc97a279d9fe8b9b6f07e1
|
Subproject commit 147f44e608f39f04af3b3be9aee1ad662e5a84e1
|
||||||
|
|
@ -43,6 +43,7 @@ class OnboardingState {
|
||||||
this.downloadAuthToken,
|
this.downloadAuthToken,
|
||||||
this.serverRegistered = false,
|
this.serverRegistered = false,
|
||||||
this.encryptionKey,
|
this.encryptionKey,
|
||||||
|
this.emailRecoveryRequested = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory OnboardingState.fromJson(Map<String, dynamic> json) =>
|
factory OnboardingState.fromJson(Map<String, dynamic> json) =>
|
||||||
|
|
@ -55,6 +56,8 @@ class OnboardingState {
|
||||||
List<int>? downloadAuthToken;
|
List<int>? downloadAuthToken;
|
||||||
bool serverRegistered;
|
bool serverRegistered;
|
||||||
List<int>? encryptionKey;
|
List<int>? encryptionKey;
|
||||||
|
bool emailRecoveryRequested;
|
||||||
|
|
||||||
|
|
||||||
List<ReceivedRecoveryShare> receivedShares = [];
|
List<ReceivedRecoveryShare> receivedShares = [];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,8 @@ OnboardingState _$OnboardingStateFromJson(Map<String, dynamic> json) =>
|
||||||
encryptionKey: (json['encryptionKey'] as List<dynamic>?)
|
encryptionKey: (json['encryptionKey'] as List<dynamic>?)
|
||||||
?.map((e) => (e as num).toInt())
|
?.map((e) => (e as num).toInt())
|
||||||
.toList(),
|
.toList(),
|
||||||
|
emailRecoveryRequested:
|
||||||
|
json['emailRecoveryRequested'] as bool? ?? false,
|
||||||
)
|
)
|
||||||
..receivedShares = (json['receivedShares'] as List<dynamic>)
|
..receivedShares = (json['receivedShares'] as List<dynamic>)
|
||||||
.map((e) => ReceivedRecoveryShare.fromJson(e as Map<String, dynamic>))
|
.map((e) => ReceivedRecoveryShare.fromJson(e as Map<String, dynamic>))
|
||||||
|
|
@ -60,5 +62,6 @@ Map<String, dynamic> _$OnboardingStateToJson(OnboardingState instance) =>
|
||||||
'downloadAuthToken': instance.downloadAuthToken,
|
'downloadAuthToken': instance.downloadAuthToken,
|
||||||
'serverRegistered': instance.serverRegistered,
|
'serverRegistered': instance.serverRegistered,
|
||||||
'encryptionKey': instance.encryptionKey,
|
'encryptionKey': instance.encryptionKey,
|
||||||
|
'emailRecoveryRequested': instance.emailRecoveryRequested,
|
||||||
'receivedShares': instance.receivedShares,
|
'receivedShares': instance.receivedShares,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,10 @@ final routerProvider = GoRouter(
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: Routes.recoverPasswordless,
|
path: Routes.recoverPasswordless,
|
||||||
builder: (context, state) => const RecoverPasswordless(),
|
builder: (context, state) {
|
||||||
|
final token = state.extra as String?;
|
||||||
|
return RecoverPasswordless(initialEmailToken: token);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
// Chats
|
// Chats
|
||||||
|
|
|
||||||
|
|
@ -776,6 +776,34 @@ class ApiService {
|
||||||
return sendRequestSync(req);
|
return sendRequestSync(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Result> getServerKeyForPasswordlessRecovery({
|
||||||
|
required int userId,
|
||||||
|
List<int>? encryptedServerKeyNone,
|
||||||
|
List<int>? pinUnlockToken,
|
||||||
|
List<int>? pinProtectionKey,
|
||||||
|
String? email,
|
||||||
|
}) async {
|
||||||
|
final get = Handshake_GetServerKeyForPasswordLessRecovery()
|
||||||
|
..userId = Int64(userId);
|
||||||
|
if (encryptedServerKeyNone != null) {
|
||||||
|
get.encryptedServerKeyNone = encryptedServerKeyNone;
|
||||||
|
}
|
||||||
|
if (pinUnlockToken != null) {
|
||||||
|
get.pinUnlockToken = pinUnlockToken;
|
||||||
|
}
|
||||||
|
if (pinProtectionKey != null) {
|
||||||
|
get.pinProtectionKey = pinProtectionKey;
|
||||||
|
}
|
||||||
|
if (email != null) {
|
||||||
|
get.email = email;
|
||||||
|
}
|
||||||
|
|
||||||
|
final handshake = Handshake()..getServerKeyForPasswordlessRecovery = get;
|
||||||
|
final req = createClientToServerFromHandshake(handshake);
|
||||||
|
return sendRequestSync(req, authenticated: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Future<Result> submitRecoveryShare({
|
Future<Result> submitRecoveryShare({
|
||||||
required String notificationId,
|
required String notificationId,
|
||||||
required List<int> encryptedMessage,
|
required List<int> encryptedMessage,
|
||||||
|
|
|
||||||
|
|
@ -325,6 +325,26 @@ class BackupService {
|
||||||
return _nextBackupStage();
|
return _nextBackupStage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<RecoveryError?> startPasswordlessBackupRecovery(
|
||||||
|
int userId,
|
||||||
|
String username,
|
||||||
|
Uint8List keyManagerBytes,
|
||||||
|
) async {
|
||||||
|
final state = BackupRecovery(
|
||||||
|
username: username,
|
||||||
|
password: '',
|
||||||
|
userId: userId,
|
||||||
|
)..state = BackupRecoveryState.archiveBackupStarted;
|
||||||
|
|
||||||
|
await deleteLocalUserData();
|
||||||
|
|
||||||
|
// Import KeyManager keys into secure storage & in-memory key manager
|
||||||
|
await RustKeyManager.importSerialized(serializedBytes: keyManagerBytes);
|
||||||
|
|
||||||
|
await KeyValueStore.put(KeyValueKeys.backupRecoveryState, state.toJson());
|
||||||
|
return _nextBackupStage();
|
||||||
|
}
|
||||||
|
|
||||||
static Future<(Uint8List?, RecoveryError?)> _downloadBackup(
|
static Future<(Uint8List?, RecoveryError?)> _downloadBackup(
|
||||||
String backupServerUrl,
|
String backupServerUrl,
|
||||||
) async {
|
) async {
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,12 @@ import 'package:cryptography_plus/cryptography_plus.dart'
|
||||||
show Hmac, Mac, SecretBox, SecretKey, Xchacha20;
|
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:go_router/go_router.dart';
|
||||||
import 'package:twonly/core/bridge/wrapper.dart';
|
import 'package:twonly/core/bridge/wrapper.dart';
|
||||||
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
|
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/keyvalue.keys.dart';
|
import 'package:twonly/src/constants/keyvalue.keys.dart';
|
||||||
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
import 'package:twonly/src/database/daos/contacts.dao.dart'
|
import 'package:twonly/src/database/daos/contacts.dao.dart'
|
||||||
show getContactDisplayName;
|
show getContactDisplayName;
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
|
|
@ -33,6 +35,9 @@ import 'package:twonly/src/visual/views/settings/backup/passwordless_recovery/he
|
||||||
enum SecondFactorType { email, pin, none }
|
enum SecondFactorType { email, pin, none }
|
||||||
|
|
||||||
class PasswordlessRecoveryService {
|
class PasswordlessRecoveryService {
|
||||||
|
static final StreamController<String> onEmailTokenReceived =
|
||||||
|
StreamController<String>.broadcast();
|
||||||
|
|
||||||
static String linkPrefix = 'https://me.twonly.eu/r/#';
|
static String linkPrefix = 'https://me.twonly.eu/r/#';
|
||||||
|
|
||||||
static final Set<String> _handledNotificationIds = {};
|
static final Set<String> _handledNotificationIds = {};
|
||||||
|
|
@ -43,7 +48,16 @@ class PasswordlessRecoveryService {
|
||||||
|
|
||||||
final fragment = link.substring(hashIndex + 1);
|
final fragment = link.substring(hashIndex + 1);
|
||||||
final parts = fragment.split('/');
|
final parts = fragment.split('/');
|
||||||
if (parts.length < 2) return;
|
if (parts.length < 2) {
|
||||||
|
if (fragment.isNotEmpty) {
|
||||||
|
onEmailTokenReceived.add(fragment);
|
||||||
|
final context = rootNavigatorKey.currentContext;
|
||||||
|
if (context != null && context.mounted) {
|
||||||
|
unawaited(context.push(Routes.recoverPasswordless, extra: fragment));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final notificationId = parts[0];
|
final notificationId = parts[0];
|
||||||
final base64Key = parts[1];
|
final base64Key = parts[1];
|
||||||
|
|
@ -384,7 +398,8 @@ class PasswordlessRecoveryService {
|
||||||
clock.now().difference(lastContactHeartbeat).inHours >= 24;
|
clock.now().difference(lastContactHeartbeat).inHours >= 24;
|
||||||
|
|
||||||
if (isContactHeartbeatOlderThan24h) {
|
if (isContactHeartbeatOlderThan24h) {
|
||||||
// Get all contacts where recoveryLastHeartbeat is NULL. Then for each contacts send
|
// Get all contacts where recoveryLastHeartbeat is NULL. Then for each contacts send.
|
||||||
|
// recoveryLastHeartbeat is ONLY updated in case the contact has responded.
|
||||||
final pendingShares =
|
final pendingShares =
|
||||||
await (twonlyDB.select(twonlyDB.contacts)..where(
|
await (twonlyDB.select(twonlyDB.contacts)..where(
|
||||||
(t) =>
|
(t) =>
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import 'package:provider/provider.dart';
|
||||||
import 'package:twonly/src/localization/generated/app_localizations.dart';
|
import 'package:twonly/src/localization/generated/app_localizations.dart';
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
||||||
import 'package:twonly/src/providers/settings.provider.dart';
|
import 'package:twonly/src/providers/settings.provider.dart';
|
||||||
|
import 'package:twonly/src/services/backup.service.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
|
||||||
|
|
@ -428,3 +429,20 @@ String createEmailHint(String email) {
|
||||||
final restDomain = domainParts.skip(1).join('.');
|
final restDomain = domainParts.skip(1).join('.');
|
||||||
return '$localMasked@$domainNameMasked${restDomain.isNotEmpty ? '.$restDomain' : ''}';
|
return '$localMasked@$domainNameMasked${restDomain.isNotEmpty ? '.$restDomain' : ''}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension RecoveryErrorLocalization on RecoveryError {
|
||||||
|
String toLocalizedString(BuildContext context) {
|
||||||
|
switch (this) {
|
||||||
|
case RecoveryError.noInternet:
|
||||||
|
return context.lang.recoverErrorNoInternet;
|
||||||
|
case RecoveryError.usernameNotValid:
|
||||||
|
return context.lang.recoverErrorUsernameNotValid;
|
||||||
|
case RecoveryError.passwordInvalid:
|
||||||
|
return context.lang.recoverErrorPasswordInvalid;
|
||||||
|
case RecoveryError.tryAgainLater:
|
||||||
|
return context.lang.recoverErrorTryAgainLater;
|
||||||
|
case RecoveryError.unkownError:
|
||||||
|
return context.lang.recoverErrorUnknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,23 +39,10 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
String errorMessage;
|
|
||||||
switch (error) {
|
|
||||||
case RecoveryError.noInternet:
|
|
||||||
errorMessage = context.lang.recoverErrorNoInternet;
|
|
||||||
case RecoveryError.usernameNotValid:
|
|
||||||
errorMessage = context.lang.recoverErrorUsernameNotValid;
|
|
||||||
case RecoveryError.passwordInvalid:
|
|
||||||
errorMessage = context.lang.recoverErrorPasswordInvalid;
|
|
||||||
case RecoveryError.tryAgainLater:
|
|
||||||
errorMessage = context.lang.recoverErrorTryAgainLater;
|
|
||||||
case RecoveryError.unkownError:
|
|
||||||
errorMessage = context.lang.recoverErrorUnknown;
|
|
||||||
}
|
|
||||||
setState(() {
|
setState(() {
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
});
|
});
|
||||||
return showSnackbar(context, errorMessage);
|
return showSnackbar(context, error.toLocalizedString(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
await Restart.restartApp(
|
await Restart.restartApp(
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,25 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:cryptography_plus/cryptography_plus.dart'
|
||||||
|
show Hmac, Mac, SecretBox, SecretKey, Xchacha20;
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:hashlib/random.dart';
|
import 'package:hashlib/random.dart';
|
||||||
import 'package:qr_flutter/qr_flutter.dart';
|
import 'package:qr_flutter/qr_flutter.dart';
|
||||||
|
import 'package:restart_app/restart_app.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
import 'package:twonly/core/bridge/wrapper.dart' show RustUtils;
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/keyvalue.keys.dart';
|
import 'package:twonly/src/constants/keyvalue.keys.dart';
|
||||||
import 'package:twonly/src/model/json/onboarding_state.model.dart';
|
import 'package:twonly/src/model/json/onboarding_state.model.dart';
|
||||||
|
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
||||||
|
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
|
||||||
|
as server;
|
||||||
|
import 'package:twonly/src/model/protobuf/client/generated/passwordless_recovery.pb.dart';
|
||||||
|
import 'package:twonly/src/services/backup.service.dart';
|
||||||
import 'package:twonly/src/services/passwordless_recovery.service.dart';
|
import 'package:twonly/src/services/passwordless_recovery.service.dart';
|
||||||
import 'package:twonly/src/utils/keyvalue.dart';
|
import 'package:twonly/src/utils/keyvalue.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
@ -22,7 +31,9 @@ import 'package:twonly/src/visual/elements/my_input.element.dart';
|
||||||
import 'package:twonly/src/visual/views/onboarding/components/animated_bell_icon.comp.dart';
|
import 'package:twonly/src/visual/views/onboarding/components/animated_bell_icon.comp.dart';
|
||||||
|
|
||||||
class RecoverPasswordless extends StatefulWidget {
|
class RecoverPasswordless extends StatefulWidget {
|
||||||
const RecoverPasswordless({super.key});
|
const RecoverPasswordless({this.initialEmailToken, super.key});
|
||||||
|
|
||||||
|
final String? initialEmailToken;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<RecoverPasswordless> createState() => _RecoverPasswordlessState();
|
State<RecoverPasswordless> createState() => _RecoverPasswordlessState();
|
||||||
|
|
@ -36,15 +47,45 @@ class _RecoverPasswordlessState extends State<RecoverPasswordless> {
|
||||||
OnboardingState? _onboardingState;
|
OnboardingState? _onboardingState;
|
||||||
Timer? _pollTimer;
|
Timer? _pollTimer;
|
||||||
|
|
||||||
|
SharedSecretData? _reconstructedSecret;
|
||||||
|
bool _isReconstructing = false;
|
||||||
|
String? _reconstructionError;
|
||||||
|
|
||||||
|
bool _isRecovering = false;
|
||||||
|
final TextEditingController _secondFactorController = TextEditingController();
|
||||||
|
|
||||||
|
StreamSubscription<String>? _emailTokenSubscription;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_secondFactorController.text = widget.initialEmailToken ?? '';
|
||||||
|
_emailTokenSubscription = PasswordlessRecoveryService
|
||||||
|
.onEmailTokenReceived
|
||||||
|
.stream
|
||||||
|
.listen((token) async {
|
||||||
|
if (mounted) {
|
||||||
|
final state = _onboardingState;
|
||||||
|
if (state != null && !state.emailRecoveryRequested) {
|
||||||
|
state.emailRecoveryRequested = true;
|
||||||
|
await KeyValueStore.update<OnboardingState>(
|
||||||
|
key: KeyValueKeys.onboardingState,
|
||||||
|
update: (s) => s.emailRecoveryRequested = true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_secondFactorController.text = token;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
_initAsync();
|
_initAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_emailTokenSubscription?.cancel();
|
||||||
_pollTimer?.cancel();
|
_pollTimer?.cancel();
|
||||||
|
_secondFactorController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -55,6 +96,14 @@ class _RecoverPasswordlessState extends State<RecoverPasswordless> {
|
||||||
KeyValueKeys.onboardingState,
|
KeyValueKeys.onboardingState,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (widget.initialEmailToken != null && !state.emailRecoveryRequested) {
|
||||||
|
state.emailRecoveryRequested = true;
|
||||||
|
await KeyValueStore.update<OnboardingState>(
|
||||||
|
key: KeyValueKeys.onboardingState,
|
||||||
|
update: (s) => s.emailRecoveryRequested = true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Generate fields if they are missing
|
// 2. Generate fields if they are missing
|
||||||
if (state.notificationId == null) {
|
if (state.notificationId == null) {
|
||||||
state
|
state
|
||||||
|
|
@ -94,6 +143,8 @@ class _RecoverPasswordlessState extends State<RecoverPasswordless> {
|
||||||
|
|
||||||
// 6. If already registered, do an immediate check and start the poll timer
|
// 6. If already registered, do an immediate check and start the poll timer
|
||||||
if (state.serverRegistered) _startPollTimer();
|
if (state.serverRegistered) _startPollTimer();
|
||||||
|
|
||||||
|
_checkAndReconstruct();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.warn('Error during passwordless recovery initialization: $e');
|
Log.warn('Error during passwordless recovery initialization: $e');
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -116,7 +167,280 @@ class _RecoverPasswordlessState extends State<RecoverPasswordless> {
|
||||||
_onboardingState!,
|
_onboardingState!,
|
||||||
);
|
);
|
||||||
// _onboardingState was changed in checkAndStorePasswordlessMessages
|
// _onboardingState was changed in checkAndStorePasswordlessMessages
|
||||||
if (didUpdate && mounted) setState(() => ());
|
if (didUpdate && mounted) {
|
||||||
|
setState(() => ());
|
||||||
|
_checkAndReconstruct();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _checkAndReconstruct() {
|
||||||
|
final state = _onboardingState;
|
||||||
|
if (state != null) {
|
||||||
|
final shares = state.receivedShares;
|
||||||
|
if (shares.isNotEmpty) {
|
||||||
|
final threshold = shares.first.threshold;
|
||||||
|
if (shares.length >= threshold &&
|
||||||
|
_reconstructedSecret == null &&
|
||||||
|
!_isReconstructing) {
|
||||||
|
unawaited(_reconstructSecret());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _reconstructSecret() async {
|
||||||
|
final state = _onboardingState;
|
||||||
|
if (state == null) return;
|
||||||
|
final shares = state.receivedShares;
|
||||||
|
if (shares.isEmpty) return;
|
||||||
|
final threshold = shares.first.threshold;
|
||||||
|
if (shares.length < threshold) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isReconstructing = true;
|
||||||
|
_reconstructionError = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final shareBytesList = shares
|
||||||
|
.map((s) => Uint8List.fromList(s.sharedSecretDataBytes))
|
||||||
|
.toList();
|
||||||
|
final secretBytes = await RustUtils.recoverSecret(
|
||||||
|
shares: shareBytesList,
|
||||||
|
threshold: threshold,
|
||||||
|
);
|
||||||
|
final sharedSecretData = SharedSecretData.fromBuffer(secretBytes);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_reconstructedSecret = sharedSecretData;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('Failed to reconstruct secret: $e');
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_reconstructionError = e.toString();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isReconstructing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _recoverNow(List<ReceivedRecoveryShare> shares) async {
|
||||||
|
final reconstructed = _reconstructedSecret;
|
||||||
|
if (reconstructed == null) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isRecovering = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final userId = shares.first.myUserId;
|
||||||
|
Uint8List? serverKey;
|
||||||
|
|
||||||
|
if (reconstructed.hasPinSeed()) {
|
||||||
|
final pin = _secondFactorController.text.trim();
|
||||||
|
if (pin.isEmpty) {
|
||||||
|
showSnackbar(
|
||||||
|
context,
|
||||||
|
context.lang.passwordlessRecoveryEnterPin,
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_isRecovering = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate pinProtectionKey
|
||||||
|
final pinProtectionKey = await Hmac.sha256().calculateMac(
|
||||||
|
Uint8List.fromList(utf8.encode(pin)),
|
||||||
|
secretKey: SecretKey(reconstructed.pinSeed),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fetch serverKey
|
||||||
|
final res = await apiService.getServerKeyForPasswordlessRecovery(
|
||||||
|
userId: userId,
|
||||||
|
pinUnlockToken: reconstructed.pinUnlockToken,
|
||||||
|
pinProtectionKey: pinProtectionKey.bytes,
|
||||||
|
encryptedServerKeyNone: reconstructed.encryptedServerKeyNonce,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.isError) {
|
||||||
|
if (mounted) {
|
||||||
|
showSnackbar(
|
||||||
|
context,
|
||||||
|
context.lang.passwordlessRecoveryTestPinIncorrect,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_isRecovering = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ok = res.value as server.Response_Ok;
|
||||||
|
serverKey = Uint8List.fromList(ok.passwordlessRecoveryServerKey);
|
||||||
|
} else if (reconstructed.hasEmailHint()) {
|
||||||
|
final state = _onboardingState;
|
||||||
|
if (state == null) return;
|
||||||
|
|
||||||
|
if (!state.emailRecoveryRequested) {
|
||||||
|
// Stage 1: Send Email
|
||||||
|
final email = _secondFactorController.text.trim();
|
||||||
|
if (email.isEmpty) {
|
||||||
|
showSnackbar(
|
||||||
|
context,
|
||||||
|
context.lang.passwordlessRecoveryEnterEmail,
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_isRecovering = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch serverKey (sends recovery email)
|
||||||
|
final res = await apiService.getServerKeyForPasswordlessRecovery(
|
||||||
|
userId: userId,
|
||||||
|
email: email,
|
||||||
|
encryptedServerKeyNone: reconstructed.encryptedServerKeyNonce,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.isError) {
|
||||||
|
if (mounted) {
|
||||||
|
final isInternalError = res.error == ErrorCode.InternalError;
|
||||||
|
showSnackbar(
|
||||||
|
context,
|
||||||
|
isInternalError
|
||||||
|
? context.lang.passwordlessRecoveryNetworkError
|
||||||
|
: context.lang.passwordlessRecoveryInvalidEmail,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_isRecovering = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success - server sent recovery email. Store in state
|
||||||
|
state.emailRecoveryRequested = true;
|
||||||
|
await KeyValueStore.update<OnboardingState>(
|
||||||
|
key: KeyValueKeys.onboardingState,
|
||||||
|
update: (s) => s.emailRecoveryRequested = true,
|
||||||
|
);
|
||||||
|
|
||||||
|
_secondFactorController.clear();
|
||||||
|
if (mounted) {
|
||||||
|
showSnackbar(
|
||||||
|
context,
|
||||||
|
context.lang.passwordlessRecoveryShareSent,
|
||||||
|
level: SnackbarLevel.success,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_isRecovering = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// Stage 2: Token verification
|
||||||
|
final token = _secondFactorController.text.trim();
|
||||||
|
if (token.isEmpty) {
|
||||||
|
if (mounted) {
|
||||||
|
showSnackbar(
|
||||||
|
context,
|
||||||
|
'Please enter the recovery token from your email.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_isRecovering = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
serverKey = base64Url.decode(base64Url.normalize(token));
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
showSnackbar(
|
||||||
|
context,
|
||||||
|
'Invalid verification token format.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_isRecovering = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt recoveryData using serverKey if present
|
||||||
|
List<int> recoveryDataBytes;
|
||||||
|
if (serverKey != null) {
|
||||||
|
final envelope = EncryptedEnvelope.fromBuffer(
|
||||||
|
reconstructed.recoveryData,
|
||||||
|
);
|
||||||
|
final secretBox = SecretBox(
|
||||||
|
envelope.encryptedData,
|
||||||
|
nonce: envelope.iv,
|
||||||
|
mac: Mac(envelope.mac),
|
||||||
|
);
|
||||||
|
final xchacha20 = Xchacha20.poly1305Aead();
|
||||||
|
recoveryDataBytes = await xchacha20.decrypt(
|
||||||
|
secretBox,
|
||||||
|
secretKey: SecretKey(serverKey),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
recoveryDataBytes = reconstructed.recoveryData;
|
||||||
|
}
|
||||||
|
|
||||||
|
final recoveryData = RecoveryData.fromBuffer(recoveryDataBytes);
|
||||||
|
|
||||||
|
// Start full passwordless recovery
|
||||||
|
final error = await BackupService.startPasswordlessBackupRecovery(
|
||||||
|
recoveryData.userId.toInt(),
|
||||||
|
shares.first.myDisplayName,
|
||||||
|
Uint8List.fromList(recoveryData.keyManager),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
if (error != null) {
|
||||||
|
showSnackbar(
|
||||||
|
context,
|
||||||
|
error.toLocalizedString(context),
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_isRecovering = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Successful! Restart the app to apply restored keymanager/archive database
|
||||||
|
await Restart.restartApp(
|
||||||
|
notificationTitle: context.lang.recoverSuccessTitle,
|
||||||
|
notificationBody: context.lang.recoverSuccessBody,
|
||||||
|
forceKill: true,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('Failed to recover passwordless: $e');
|
||||||
|
if (mounted) {
|
||||||
|
showSnackbar(
|
||||||
|
context,
|
||||||
|
'${context.lang.recoverErrorUnknown}: $e',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isRecovering = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _registerPasswordlessNotification(OnboardingState state) async {
|
Future<void> _registerPasswordlessNotification(OnboardingState state) async {
|
||||||
|
|
@ -233,14 +557,93 @@ class _RecoverPasswordlessState extends State<RecoverPasswordless> {
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
if (thresholdReached) ...[
|
if (thresholdReached) ...[
|
||||||
MyButton(
|
if (_isReconstructing)
|
||||||
onPressed: () {
|
const Center(
|
||||||
// TODO(recovery): Navigate to the actual recovery flow with the collected shares.
|
child: Padding(
|
||||||
Log.info('Recover now pressed with ${shares.length} shares');
|
padding: EdgeInsets.symmetric(vertical: 24),
|
||||||
},
|
child: CircularProgressIndicator(),
|
||||||
child: Text(context.lang.recoverPasswordlessRecoverNowBtn),
|
),
|
||||||
|
)
|
||||||
|
else if (_reconstructionError != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
child: Text(
|
||||||
|
'Reconstruction failed: $_reconstructionError',
|
||||||
|
style: const TextStyle(color: Colors.red),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else if (_reconstructedSecret != null) ...[
|
||||||
|
if (_reconstructedSecret!.hasPinSeed()) ...[
|
||||||
|
MyInput(
|
||||||
|
controller: _secondFactorController,
|
||||||
|
hintText: context.lang.passwordlessRecoveryMethodPinHint,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
] else if (_reconstructedSecret!.hasEmailHint()) ...[
|
||||||
|
if (_onboardingState != null &&
|
||||||
|
!_onboardingState!.emailRecoveryRequested) ...[
|
||||||
|
MyInput(
|
||||||
|
controller: _secondFactorController,
|
||||||
|
hintText: _reconstructedSecret!.emailHint,
|
||||||
|
prefixIcon: const Icon(Icons.email_rounded),
|
||||||
|
keyboardType: TextInputType.emailAddress,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
] else ...[
|
||||||
|
MyInput(
|
||||||
|
controller: _secondFactorController,
|
||||||
|
hintText: 'Enter recovery token',
|
||||||
|
prefixIcon: const Icon(Icons.key_rounded),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
MyButton(
|
||||||
|
onPressed: _isRecovering ? null : () => _recoverNow(shares),
|
||||||
|
child: _isRecovering
|
||||||
|
? const SizedBox(
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2.5,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
_reconstructedSecret!.hasEmailHint() &&
|
||||||
|
_onboardingState != null &&
|
||||||
|
!_onboardingState!.emailRecoveryRequested
|
||||||
|
? 'Send recovery email'
|
||||||
|
: context.lang.recoverPasswordlessRecoverNowBtn,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
if (_reconstructedSecret!.hasEmailHint() &&
|
||||||
|
_onboardingState != null &&
|
||||||
|
_onboardingState!.emailRecoveryRequested) ...[
|
||||||
|
MyButton(
|
||||||
|
onPressed: _isRecovering
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
final state = _onboardingState;
|
||||||
|
if (state == null) return;
|
||||||
|
setState(() {
|
||||||
|
state.emailRecoveryRequested = false;
|
||||||
|
_secondFactorController.clear();
|
||||||
|
});
|
||||||
|
await KeyValueStore.update<OnboardingState>(
|
||||||
|
key: KeyValueKeys.onboardingState,
|
||||||
|
update: (s) => s.emailRecoveryRequested = false,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
variant: MyButtonVariant.secondary,
|
||||||
|
child: Text(context.lang.passwordlessRecoveryResendEmail),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -30,22 +30,9 @@ class _RecoveryViewState extends State<RecoveryView> {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
String msg;
|
|
||||||
switch (error) {
|
|
||||||
case RecoveryError.noInternet:
|
|
||||||
msg = context.lang.recoverErrorNoInternet;
|
|
||||||
case RecoveryError.usernameNotValid:
|
|
||||||
msg = context.lang.recoverErrorUsernameNotValid;
|
|
||||||
case RecoveryError.passwordInvalid:
|
|
||||||
msg = context.lang.recoverErrorPasswordInvalid;
|
|
||||||
case RecoveryError.tryAgainLater:
|
|
||||||
msg = context.lang.recoverErrorTryAgainLater;
|
|
||||||
case RecoveryError.unkownError:
|
|
||||||
msg = context.lang.recoverErrorUnknown;
|
|
||||||
}
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
_errorMessage = msg;
|
_errorMessage = error.toLocalizedString(context);
|
||||||
_showRegisterNewPrompt = true;
|
_showRegisterNewPrompt = true;
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -116,4 +116,13 @@ impl RustKeyManager {
|
||||||
let serialized_bytes = postcard::to_allocvec(&*key_manager)?;
|
let serialized_bytes = postcard::to_allocvec(&*key_manager)?;
|
||||||
Ok(serialized_bytes)
|
Ok(serialized_bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn import_serialized(serialized_bytes: Vec<u8>) -> Result<()> {
|
||||||
|
let ctx = get_twonly_flutter()?;
|
||||||
|
let key_manager: crate::keys::KeyManager = postcard::from_bytes(&serialized_bytes)?;
|
||||||
|
key_manager.store_to_keychain(&ctx.secure_storage)?;
|
||||||
|
*ctx.key_manager.lock().await = key_manager;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
|
||||||
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
||||||
);
|
);
|
||||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
|
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
|
||||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -508562557;
|
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 340781866;
|
||||||
|
|
||||||
// Section: executor
|
// Section: executor
|
||||||
|
|
||||||
|
|
@ -469,6 +469,46 @@ fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_impl(
|
||||||
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
rust_vec_len_: i32,
|
||||||
|
data_len_: i32,
|
||||||
|
) {
|
||||||
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
|
||||||
|
flutter_rust_bridge::for_generated::TaskInfo {
|
||||||
|
debug_name: "rust_key_manager_import_serialized",
|
||||||
|
port: Some(port_),
|
||||||
|
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||||
|
},
|
||||||
|
move || {
|
||||||
|
let message = unsafe {
|
||||||
|
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||||
|
ptr_,
|
||||||
|
rust_vec_len_,
|
||||||
|
data_len_,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut deserializer =
|
||||||
|
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
|
let api_serialized_bytes = <Vec<u8>>::sse_decode(&mut deserializer);
|
||||||
|
deserializer.end();
|
||||||
|
move |context| async move {
|
||||||
|
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>(
|
||||||
|
(move || async move {
|
||||||
|
let output_ok =
|
||||||
|
crate::bridge::wrapper::key_manager::RustKeyManager::import_serialized(
|
||||||
|
api_serialized_bytes,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(output_ok)
|
||||||
|
})()
|
||||||
|
.await,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(
|
fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(
|
||||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
|
@ -1829,32 +1869,33 @@ fn pde_ffi_dispatcher_primary_impl(
|
||||||
18 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_login_token_impl(port, ptr, rust_vec_len, data_len),
|
18 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_login_token_impl(port, ptr, rust_vec_len, data_len),
|
||||||
19 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len),
|
19 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len),
|
||||||
20 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len),
|
20 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len),
|
||||||
21 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len),
|
21 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_impl(port, ptr, rust_vec_len, data_len),
|
||||||
22 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_prekey_impl(port, ptr, rust_vec_len, data_len),
|
22 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len),
|
||||||
23 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_prekeys_impl(port, ptr, rust_vec_len, data_len),
|
23 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_prekey_impl(port, ptr, rust_vec_len, data_len),
|
||||||
24 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_key_manager_impl(port, ptr, rust_vec_len, data_len),
|
24 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_prekeys_impl(port, ptr, rust_vec_len, data_len),
|
||||||
25 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_signed_prekey_impl(port, ptr, rust_vec_len, data_len),
|
25 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_key_manager_impl(port, ptr, rust_vec_len, data_len),
|
||||||
26 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(port, ptr, rust_vec_len, data_len),
|
26 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_signed_prekey_impl(port, ptr, rust_vec_len, data_len),
|
||||||
27 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len),
|
27 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(port, ptr, rust_vec_len, data_len),
|
||||||
28 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_store_signed_prekey_impl(port, ptr, rust_vec_len, data_len),
|
28 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len),
|
||||||
29 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len),
|
29 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_store_signed_prekey_impl(port, ptr, rust_vec_len, data_len),
|
||||||
30 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len),
|
30 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len),
|
||||||
31 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_announced_user_by_public_id_impl(port, ptr, rust_vec_len, data_len),
|
31 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len),
|
||||||
32 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_config_impl(port, ptr, rust_vec_len, data_len),
|
32 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_announced_user_by_public_id_impl(port, ptr, rust_vec_len, data_len),
|
||||||
33 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_contact_promotion_impl(port, ptr, rust_vec_len, data_len),
|
33 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_config_impl(port, ptr, rust_vec_len, data_len),
|
||||||
34 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_contact_version_impl(port, ptr, rust_vec_len, data_len),
|
34 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_contact_promotion_impl(port, ptr, rust_vec_len, data_len),
|
||||||
35 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_other_promotions_by_public_id_impl(port, ptr, rust_vec_len, data_len),
|
35 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_contact_version_impl(port, ptr, rust_vec_len, data_len),
|
||||||
36 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_own_promotions_after_version_impl(port, ptr, rust_vec_len, data_len),
|
36 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_other_promotions_by_public_id_impl(port, ptr, rust_vec_len, data_len),
|
||||||
37 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_share_for_contact_impl(port, ptr, rust_vec_len, data_len),
|
37 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_own_promotions_after_version_impl(port, ptr, rust_vec_len, data_len),
|
||||||
38 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_push_new_user_relation_impl(port, ptr, rust_vec_len, data_len),
|
38 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_share_for_contact_impl(port, ptr, rust_vec_len, data_len),
|
||||||
39 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_push_own_promotion_and_clear_old_version_impl(port, ptr, rust_vec_len, data_len),
|
39 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_push_new_user_relation_impl(port, ptr, rust_vec_len, data_len),
|
||||||
40 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_set_contact_version_impl(port, ptr, rust_vec_len, data_len),
|
40 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_push_own_promotion_and_clear_old_version_impl(port, ptr, rust_vec_len, data_len),
|
||||||
41 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_set_shares_impl(port, ptr, rust_vec_len, data_len),
|
41 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_set_contact_version_impl(port, ptr, rust_vec_len, data_len),
|
||||||
42 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_store_other_promotion_impl(port, ptr, rust_vec_len, data_len),
|
42 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_set_shares_impl(port, ptr, rust_vec_len, data_len),
|
||||||
43 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_update_config_impl(port, ptr, rust_vec_len, data_len),
|
43 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_store_other_promotion_impl(port, ptr, rust_vec_len, data_len),
|
||||||
44 => wire__crate__bridge__callbacks__user_discovery__user_discovery_utils_flutter_sign_data_impl(port, ptr, rust_vec_len, data_len),
|
44 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_update_config_impl(port, ptr, rust_vec_len, data_len),
|
||||||
45 => wire__crate__bridge__callbacks__user_discovery__user_discovery_utils_flutter_verify_signature_impl(port, ptr, rust_vec_len, data_len),
|
45 => wire__crate__bridge__callbacks__user_discovery__user_discovery_utils_flutter_sign_data_impl(port, ptr, rust_vec_len, data_len),
|
||||||
46 => wire__crate__bridge__callbacks__user_discovery__user_discovery_utils_flutter_verify_stored_pubkey_impl(port, ptr, rust_vec_len, data_len),
|
46 => wire__crate__bridge__callbacks__user_discovery__user_discovery_utils_flutter_verify_signature_impl(port, ptr, rust_vec_len, data_len),
|
||||||
|
47 => wire__crate__bridge__callbacks__user_discovery__user_discovery_utils_flutter_verify_stored_pubkey_impl(port, ptr, rust_vec_len, data_len),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:twonly/core/bridge.dart' as bridge;
|
import 'package:twonly/core/bridge.dart' as bridge;
|
||||||
import 'package:twonly/core/bridge/wrapper/backup.dart';
|
import 'package:twonly/core/bridge/wrapper/backup.dart';
|
||||||
|
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
|
||||||
import 'package:twonly/core/frb_generated.dart';
|
import 'package:twonly/core/frb_generated.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
|
|
@ -20,6 +21,8 @@ import 'package:twonly/src/services/backup.service.dart';
|
||||||
import 'package:twonly/src/services/user.service.dart';
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
import 'package:twonly/src/utils/keyvalue.dart';
|
import 'package:twonly/src/utils/keyvalue.dart';
|
||||||
|
|
||||||
|
import '../mocks/platform_channels.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
if (!Platform.isMacOS) {
|
if (!Platform.isMacOS) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -40,6 +43,17 @@ void main() {
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pathProviderChannel = MethodChannel(
|
||||||
|
'plugins.flutter.io/path_provider',
|
||||||
|
);
|
||||||
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
|
.setMockMethodCallHandler(pathProviderChannel, (methodCall) async {
|
||||||
|
if (methodCall.method == 'getApplicationSupportDirectory') {
|
||||||
|
return tempDir.path;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
final dylibPath =
|
final dylibPath =
|
||||||
'${Directory.current.path}/rust/target/debug/librust_lib_twonly.dylib';
|
'${Directory.current.path}/rust/target/debug/librust_lib_twonly.dylib';
|
||||||
if (File(dylibPath).existsSync()) {
|
if (File(dylibPath).existsSync()) {
|
||||||
|
|
@ -66,6 +80,7 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
|
setupPlatformChannelMocks();
|
||||||
await locator.reset();
|
await locator.reset();
|
||||||
final dbFile = File('${tempDir.path}/twonly.sqlite');
|
final dbFile = File('${tempDir.path}/twonly.sqlite');
|
||||||
locator
|
locator
|
||||||
|
|
@ -230,5 +245,23 @@ void main() {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'startPasswordlessBackupRecovery restores identity and returns tryAgainLater on download',
|
||||||
|
() async {
|
||||||
|
final keyManagerBytes = await RustKeyManager.serialize();
|
||||||
|
|
||||||
|
final error = await BackupService.startPasswordlessBackupRecovery(
|
||||||
|
1,
|
||||||
|
'test_user',
|
||||||
|
keyManagerBytes,
|
||||||
|
);
|
||||||
|
expect(error, RecoveryError.tryAgainLater);
|
||||||
|
|
||||||
|
// Verify key manager was imported successfully
|
||||||
|
final recoveredUserId = await RustKeyManager.getUserId();
|
||||||
|
expect(recoveredUserId, 1);
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -362,11 +362,13 @@ void main() {
|
||||||
|
|
||||||
// Verify db update
|
// Verify db update
|
||||||
final c2 = await twonlyDB.contactsDao.getContactById(2);
|
final c2 = await twonlyDB.contactsDao.getContactById(2);
|
||||||
expect(c2?.recoveryLastHeartbeat, baseTime);
|
expect(c2?.recoveryLastHeartbeat, isNull);
|
||||||
|
|
||||||
// Verify calling again does not resend
|
// Verify calling again does not resend
|
||||||
mockApi.sentTextMessages.clear();
|
mockApi.sentTextMessages.clear();
|
||||||
|
await withClock(Clock.fixed(baseTime), () async {
|
||||||
await PasswordlessRecoveryService.performHeartbeat();
|
await PasswordlessRecoveryService.performHeartbeat();
|
||||||
|
});
|
||||||
expect(mockApi.sentTextMessages.length, 0);
|
expect(mockApi.sentTextMessages.length, 0);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue