mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-10-09 02:54:14 +00:00
add version to manual verifiy numbers
This commit is contained in:
parent
3bc446553a
commit
5786effc5b
27 changed files with 1644 additions and 257 deletions
2
.github/workflows/dev_github.yml
vendored
2
.github/workflows/dev_github.yml
vendored
|
|
@ -4,7 +4,7 @@ on:
|
|||
workflow_dispatch: {}
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- disabled_dev
|
||||
paths:
|
||||
- lib/**/*.dart
|
||||
|
||||
|
|
|
|||
12
lib/app.dart
12
lib/app.dart
|
|
@ -192,16 +192,18 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
|||
HomeViewState.streamSharedLink.add(uri);
|
||||
}
|
||||
|
||||
// Subscribe to all events (initial link and further)
|
||||
_deepLinkSub = AppLinks().uriLinkStream.listen((uri) async {
|
||||
Future<void> handleIncomingUrl(Uri uri) async {
|
||||
if (!mounted) return;
|
||||
Log.info('Got link via app links: ${uri.scheme}');
|
||||
Log.info('Handling incoming link: ${uri.scheme}://${uri.host}');
|
||||
if (!await handleIntentUrl(context, uri)) {
|
||||
if (uri.scheme.startsWith('http')) {
|
||||
handleShareLink(uri);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Subscribe to all events (initial link and further)
|
||||
_deepLinkSub = AppLinks().uriLinkStream.listen(handleIncomingUrl);
|
||||
|
||||
void handleShareMedia(String path, MediaType type) {
|
||||
HomeViewState.pendingSharedMedia = (path, type);
|
||||
|
|
@ -212,7 +214,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
|||
|
||||
_intentStreamSub = initIntentStreams(
|
||||
context,
|
||||
handleShareLink,
|
||||
handleIncomingUrl,
|
||||
handleShareMedia,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,11 @@ class RustSignal {
|
|||
contactId: contactId,
|
||||
);
|
||||
|
||||
static Future<String?> getSafetyNumber({required PlatformInt64 contactId}) =>
|
||||
RustLib.instance.api.crateBridgeWrapperSignalRustSignalGetSafetyNumber(
|
||||
contactId: contactId,
|
||||
);
|
||||
|
||||
static Future<Uint8List> getUserPublicKey() =>
|
||||
RustLib.instance.api.crateBridgeWrapperSignalRustSignalGetUserPublicKey();
|
||||
|
||||
|
|
@ -57,6 +62,14 @@ class RustSignal {
|
|||
bundle: bundle,
|
||||
);
|
||||
|
||||
/// Removes the current session with a contact and immediately establishes
|
||||
/// a fresh one while requiring the contact's stored identity key to match.
|
||||
static Future<void> resetContactSession({required PlatformInt64 contactId}) =>
|
||||
RustLib.instance.api
|
||||
.crateBridgeWrapperSignalRustSignalResetContactSession(
|
||||
contactId: contactId,
|
||||
);
|
||||
|
||||
@override
|
||||
int get hashCode => 0;
|
||||
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
|
|||
String get codegenVersion => '2.12.0';
|
||||
|
||||
@override
|
||||
int get rustContentHash => -2068471902;
|
||||
int get rustContentHash => -1675944024;
|
||||
|
||||
static const kDefaultExternalLibraryLoaderConfig =
|
||||
ExternalLibraryLoaderConfig(
|
||||
|
|
@ -699,6 +699,10 @@ abstract class RustLibApi extends BaseApi {
|
|||
required PlatformInt64 contactId,
|
||||
});
|
||||
|
||||
Future<String?> crateBridgeWrapperSignalRustSignalGetSafetyNumber({
|
||||
required PlatformInt64 contactId,
|
||||
});
|
||||
|
||||
Future<Uint8List> crateBridgeWrapperSignalRustSignalGetUserPublicKey();
|
||||
|
||||
Future<void> crateBridgeWrapperSignalRustSignalProcessPrekeyBundle({
|
||||
|
|
@ -707,6 +711,10 @@ abstract class RustLibApi extends BaseApi {
|
|||
required FrbPreKeyBundle bundle,
|
||||
});
|
||||
|
||||
Future<void> crateBridgeWrapperSignalRustSignalResetContactSession({
|
||||
required PlatformInt64 contactId,
|
||||
});
|
||||
|
||||
Future<List<Uint8List>> crateBridgeWrapperRustUtilsGenerateShares({
|
||||
required List<int> secret,
|
||||
required int total,
|
||||
|
|
@ -6206,6 +6214,40 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
argNames: ["contactId"],
|
||||
);
|
||||
|
||||
@override
|
||||
Future<String?> crateBridgeWrapperSignalRustSignalGetSafetyNumber({
|
||||
required PlatformInt64 contactId,
|
||||
}) {
|
||||
return handler.executeNormal(
|
||||
NormalTask(
|
||||
callFfi: (port_) {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
sse_encode_i_64(contactId, serializer);
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 159,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_opt_String,
|
||||
decodeErrorData: sse_decode_AnyhowException,
|
||||
),
|
||||
constMeta: kCrateBridgeWrapperSignalRustSignalGetSafetyNumberConstMeta,
|
||||
argValues: [contactId],
|
||||
apiImpl: this,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TaskConstMeta
|
||||
get kCrateBridgeWrapperSignalRustSignalGetSafetyNumberConstMeta =>
|
||||
const TaskConstMeta(
|
||||
debugName: "rust_signal_get_safety_number",
|
||||
argNames: ["contactId"],
|
||||
);
|
||||
|
||||
@override
|
||||
Future<Uint8List> crateBridgeWrapperSignalRustSignalGetUserPublicKey() {
|
||||
return handler.executeNormal(
|
||||
|
|
@ -6215,7 +6257,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 159,
|
||||
funcId: 160,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6253,7 +6295,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 160,
|
||||
funcId: 161,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6276,6 +6318,41 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
argNames: ["name", "deviceId", "bundle"],
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> crateBridgeWrapperSignalRustSignalResetContactSession({
|
||||
required PlatformInt64 contactId,
|
||||
}) {
|
||||
return handler.executeNormal(
|
||||
NormalTask(
|
||||
callFfi: (port_) {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
sse_encode_i_64(contactId, serializer);
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 162,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_unit,
|
||||
decodeErrorData: sse_decode_AnyhowException,
|
||||
),
|
||||
constMeta:
|
||||
kCrateBridgeWrapperSignalRustSignalResetContactSessionConstMeta,
|
||||
argValues: [contactId],
|
||||
apiImpl: this,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TaskConstMeta
|
||||
get kCrateBridgeWrapperSignalRustSignalResetContactSessionConstMeta =>
|
||||
const TaskConstMeta(
|
||||
debugName: "rust_signal_reset_contact_session",
|
||||
argNames: ["contactId"],
|
||||
);
|
||||
|
||||
@override
|
||||
Future<List<Uint8List>> crateBridgeWrapperRustUtilsGenerateShares({
|
||||
required List<int> secret,
|
||||
|
|
@ -6292,7 +6369,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 161,
|
||||
funcId: 163,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6327,7 +6404,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 162,
|
||||
funcId: 164,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6372,7 +6449,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 163,
|
||||
funcId: 165,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6415,7 +6492,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 164,
|
||||
funcId: 166,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6449,7 +6526,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 165,
|
||||
funcId: 167,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6484,7 +6561,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 166,
|
||||
funcId: 168,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6519,7 +6596,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 167,
|
||||
funcId: 169,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6552,7 +6629,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
return pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 168,
|
||||
funcId: 170,
|
||||
)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
|
|
@ -6592,7 +6669,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 169,
|
||||
funcId: 171,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6637,7 +6714,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 170,
|
||||
funcId: 172,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6667,7 +6744,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 171,
|
||||
funcId: 173,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6700,7 +6777,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 172,
|
||||
funcId: 174,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6735,7 +6812,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 173,
|
||||
funcId: 175,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6774,7 +6851,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
return pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 174,
|
||||
funcId: 176,
|
||||
)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
|
|
|
|||
|
|
@ -138,6 +138,10 @@ class UserConfig {
|
|||
bool autoStoreAllSendUnlimitedMediaFiles;
|
||||
bool typingIndicators;
|
||||
bool showRestoreFlame;
|
||||
|
||||
/// Unused: the best friend is derived from `total_media_counter` whenever
|
||||
/// it is needed (`Group::best_friend_group_id`). Kept so configurations
|
||||
/// written by older versions still deserialize.
|
||||
String? myBestFriendGroupId;
|
||||
DateTime? signalLastSignedPreKeyUpdated;
|
||||
DateTime? signalLastPqcPreKeysUploaded;
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ class Routes {
|
|||
static const String settingsHelpFaq = '/settings/help/faq';
|
||||
static const String settingsHelpFaqVerifyBadge =
|
||||
'/settings/help/faq/verifybadge';
|
||||
static const String settingsHelpFaqManualVerification =
|
||||
'/settings/help/faq/verifybadge/manual';
|
||||
static const String settingsHelpContactUs = '/settings/help/contact_us';
|
||||
static const String settingsHelpNews = '/settings/help/news';
|
||||
static const String settingsHelpDiagnostics = '/settings/help/diagnostics';
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ enum VerificationType {
|
|||
migratedFromOldVersion,
|
||||
qrScanned,
|
||||
link,
|
||||
manual,
|
||||
secretQrToken,
|
||||
contactSharedByVerified,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -968,6 +968,12 @@ abstract class AppLocalizations {
|
|||
/// **'Verified via link.'**
|
||||
String get verificationTypeLink;
|
||||
|
||||
/// No description provided for @verificationTypeManual.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Safety number compared manually.'**
|
||||
String get verificationTypeManual;
|
||||
|
||||
/// No description provided for @verificationTypeMigratedFromOldVersion.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -998,6 +1004,42 @@ abstract class AppLocalizations {
|
|||
/// **'Block'**
|
||||
String get contactBlock;
|
||||
|
||||
/// No description provided for @contactTroubleshoot.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Troubleshoot connection'**
|
||||
String get contactTroubleshoot;
|
||||
|
||||
/// No description provided for @contactResetSessionTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Reset connection with {username}?'**
|
||||
String contactResetSessionTitle(String username);
|
||||
|
||||
/// No description provided for @contactResetSessionBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'twonly will remove the current encrypted connection and establish a new one. Messages currently in transit may be lost.'**
|
||||
String get contactResetSessionBody;
|
||||
|
||||
/// No description provided for @contactResetSessionConfirm.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Reset connection'**
|
||||
String get contactResetSessionConfirm;
|
||||
|
||||
/// No description provided for @contactResetSessionSuccess.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The encrypted connection with {username} was re-established.'**
|
||||
String contactResetSessionSuccess(String username);
|
||||
|
||||
/// No description provided for @contactResetSessionFailed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The connection could not be reset. Check your internet connection and try again.'**
|
||||
String get contactResetSessionFailed;
|
||||
|
||||
/// No description provided for @contactBlockTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -2684,6 +2726,48 @@ abstract class AppLocalizations {
|
|||
/// **'The public key in the link does not match the public key stored for this contact. Try to meet your friend in person and scan the QR code directly!'**
|
||||
String get linkPubkeyDoesNotMatch;
|
||||
|
||||
/// No description provided for @verificationMissingSession.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'There is no secure session stored for this contact yet. Exchange a message first, then ask the person to share a new verification link.'**
|
||||
String get verificationMissingSession;
|
||||
|
||||
/// No description provided for @verificationLinkInvalidTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Invalid verification link'**
|
||||
String get verificationLinkInvalidTitle;
|
||||
|
||||
/// No description provided for @verificationLinkInvalid.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The verification link is incomplete or invalid. Ask the person to share it again from their twonly profile.'**
|
||||
String get verificationLinkInvalid;
|
||||
|
||||
/// No description provided for @verificationLinkErrorTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Verification unavailable'**
|
||||
String get verificationLinkErrorTitle;
|
||||
|
||||
/// No description provided for @verificationLinkError.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'twonly could not check this verification link. Please try again.'**
|
||||
String get verificationLinkError;
|
||||
|
||||
/// No description provided for @verificationOwnProfileTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This is your profile'**
|
||||
String get verificationOwnProfileTitle;
|
||||
|
||||
/// No description provided for @verificationOwnProfile.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'You scanned your own verification code.'**
|
||||
String get verificationOwnProfile;
|
||||
|
||||
/// No description provided for @startWithCameraOpen.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -2870,6 +2954,60 @@ abstract class AppLocalizations {
|
|||
/// **'A contact whose identity has *not* yet been verified.'**
|
||||
String get verificationBadgeRedDesc;
|
||||
|
||||
/// No description provided for @manualVerificationOpen.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Safety numbers'**
|
||||
String get manualVerificationOpen;
|
||||
|
||||
/// No description provided for @manualVerificationTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Compare safety number'**
|
||||
String get manualVerificationTitle;
|
||||
|
||||
/// No description provided for @manualVerificationMarkVerified.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Mark as verified'**
|
||||
String get manualVerificationMarkVerified;
|
||||
|
||||
/// No description provided for @manualVerificationDescription.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Compare this safety number with {username}. If both devices show the same number, your end-to-end encryption is verified.'**
|
||||
String manualVerificationDescription(String username);
|
||||
|
||||
/// No description provided for @manualVerificationMissingKeyTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No safety number available'**
|
||||
String get manualVerificationMissingKeyTitle;
|
||||
|
||||
/// No description provided for @manualVerificationMissingKeyDescription.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'There is no Signal session with {username} yet. Exchange a message first, then try again.'**
|
||||
String manualVerificationMissingKeyDescription(String username);
|
||||
|
||||
/// No description provided for @manualVerificationLoadFailed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The safety number could not be loaded.'**
|
||||
String get manualVerificationLoadFailed;
|
||||
|
||||
/// No description provided for @manualVerificationRetry.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Try again'**
|
||||
String get manualVerificationRetry;
|
||||
|
||||
/// No description provided for @manualVerificationSuccess.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'You compared the safety number with {username} and confirmed their identity.'**
|
||||
String manualVerificationSuccess(String username);
|
||||
|
||||
/// No description provided for @scanNow.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -481,6 +481,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get verificationTypeLink => 'Per Link verifiziert.';
|
||||
|
||||
@override
|
||||
String get verificationTypeManual => 'Sicherheitsnummer manuell verglichen.';
|
||||
|
||||
@override
|
||||
String get verificationTypeMigratedFromOldVersion =>
|
||||
'Von alter Version migriert';
|
||||
|
|
@ -497,6 +500,30 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get contactBlock => 'Blockieren';
|
||||
|
||||
@override
|
||||
String get contactTroubleshoot => 'Verbindungsprobleme beheben';
|
||||
|
||||
@override
|
||||
String contactResetSessionTitle(String username) {
|
||||
return 'Verbindung zu $username zurücksetzen?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contactResetSessionBody =>
|
||||
'twonly löscht die aktuelle verschlüsselte Verbindung und baut sie anschließend neu auf. Nachrichten, die gerade übertragen werden, können dabei verloren gehen.';
|
||||
|
||||
@override
|
||||
String get contactResetSessionConfirm => 'Verbindung zurücksetzen';
|
||||
|
||||
@override
|
||||
String contactResetSessionSuccess(String username) {
|
||||
return 'Die verschlüsselte Verbindung zu $username wurde neu aufgebaut.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contactResetSessionFailed =>
|
||||
'Die Verbindung konnte nicht zurückgesetzt werden. Prüfe deine Internetverbindung und versuche es erneut.';
|
||||
|
||||
@override
|
||||
String contactBlockTitle(Object username) {
|
||||
return 'Blockiere $username';
|
||||
|
|
@ -1504,6 +1531,31 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get linkPubkeyDoesNotMatch =>
|
||||
'Der öffentliche Schlüssel im Link stimmt nicht mit dem für diesen Kontakt gespeicherten öffentlichen Schlüssel überein. Triff die Person persönlich und scanne den QR-Code direkt!';
|
||||
|
||||
@override
|
||||
String get verificationMissingSession =>
|
||||
'Für diesen Kontakt ist noch keine sichere Sitzung gespeichert. Tauscht zuerst eine Nachricht aus und lass dir danach einen neuen Verifizierungslink senden.';
|
||||
|
||||
@override
|
||||
String get verificationLinkInvalidTitle => 'Ungültiger Verifizierungslink';
|
||||
|
||||
@override
|
||||
String get verificationLinkInvalid =>
|
||||
'Der Verifizierungslink ist unvollständig oder ungültig. Bitte die Person, ihn erneut aus ihrem twonly-Profil zu teilen.';
|
||||
|
||||
@override
|
||||
String get verificationLinkErrorTitle => 'Verifizierung nicht möglich';
|
||||
|
||||
@override
|
||||
String get verificationLinkError =>
|
||||
'twonly konnte diesen Verifizierungslink nicht prüfen. Versuche es bitte erneut.';
|
||||
|
||||
@override
|
||||
String get verificationOwnProfileTitle => 'Das ist dein Profil';
|
||||
|
||||
@override
|
||||
String get verificationOwnProfile =>
|
||||
'Du hast deinen eigenen Verifizierungscode gescannt.';
|
||||
|
||||
@override
|
||||
String get startWithCameraOpen => 'Mit geöffneter Kamera starten';
|
||||
|
||||
|
|
@ -1626,6 +1678,41 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get verificationBadgeRedDesc =>
|
||||
'Ein Kontakt, dessen Identität noch *nicht überprüft* wurde.';
|
||||
|
||||
@override
|
||||
String get manualVerificationOpen => 'Sicherheitsnummern';
|
||||
|
||||
@override
|
||||
String get manualVerificationTitle => 'Sicherheitsnummer vergleichen';
|
||||
|
||||
@override
|
||||
String get manualVerificationMarkVerified => 'Als verifiziert markieren';
|
||||
|
||||
@override
|
||||
String manualVerificationDescription(String username) {
|
||||
return 'Vergleiche diese Sicherheitsnummer mit $username. Wenn auf beiden Geräten dieselbe Nummer angezeigt wird, ist eure Ende-zu-Ende-Verschlüsselung verifiziert.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get manualVerificationMissingKeyTitle =>
|
||||
'Keine Sicherheitsnummer verfügbar';
|
||||
|
||||
@override
|
||||
String manualVerificationMissingKeyDescription(String username) {
|
||||
return 'Mit $username besteht noch keine Signal-Sitzung. Tauscht zuerst eine Nachricht aus und versucht es danach erneut.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get manualVerificationLoadFailed =>
|
||||
'Die Sicherheitsnummer konnte nicht geladen werden.';
|
||||
|
||||
@override
|
||||
String get manualVerificationRetry => 'Erneut versuchen';
|
||||
|
||||
@override
|
||||
String manualVerificationSuccess(String username) {
|
||||
return 'Du hast die Sicherheitsnummer mit $username verglichen und die Identität bestätigt.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get scanNow => 'Jetzt scannen';
|
||||
|
||||
|
|
|
|||
|
|
@ -478,6 +478,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get verificationTypeLink => 'Verified via link.';
|
||||
|
||||
@override
|
||||
String get verificationTypeManual => 'Safety number compared manually.';
|
||||
|
||||
@override
|
||||
String get verificationTypeMigratedFromOldVersion =>
|
||||
'Migrated from old version.';
|
||||
|
|
@ -494,6 +497,30 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get contactBlock => 'Block';
|
||||
|
||||
@override
|
||||
String get contactTroubleshoot => 'Troubleshoot connection';
|
||||
|
||||
@override
|
||||
String contactResetSessionTitle(String username) {
|
||||
return 'Reset connection with $username?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contactResetSessionBody =>
|
||||
'twonly will remove the current encrypted connection and establish a new one. Messages currently in transit may be lost.';
|
||||
|
||||
@override
|
||||
String get contactResetSessionConfirm => 'Reset connection';
|
||||
|
||||
@override
|
||||
String contactResetSessionSuccess(String username) {
|
||||
return 'The encrypted connection with $username was re-established.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contactResetSessionFailed =>
|
||||
'The connection could not be reset. Check your internet connection and try again.';
|
||||
|
||||
@override
|
||||
String contactBlockTitle(Object username) {
|
||||
return 'Block $username';
|
||||
|
|
@ -1492,6 +1519,31 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get linkPubkeyDoesNotMatch =>
|
||||
'The public key in the link does not match the public key stored for this contact. Try to meet your friend in person and scan the QR code directly!';
|
||||
|
||||
@override
|
||||
String get verificationMissingSession =>
|
||||
'There is no secure session stored for this contact yet. Exchange a message first, then ask the person to share a new verification link.';
|
||||
|
||||
@override
|
||||
String get verificationLinkInvalidTitle => 'Invalid verification link';
|
||||
|
||||
@override
|
||||
String get verificationLinkInvalid =>
|
||||
'The verification link is incomplete or invalid. Ask the person to share it again from their twonly profile.';
|
||||
|
||||
@override
|
||||
String get verificationLinkErrorTitle => 'Verification unavailable';
|
||||
|
||||
@override
|
||||
String get verificationLinkError =>
|
||||
'twonly could not check this verification link. Please try again.';
|
||||
|
||||
@override
|
||||
String get verificationOwnProfileTitle => 'This is your profile';
|
||||
|
||||
@override
|
||||
String get verificationOwnProfile =>
|
||||
'You scanned your own verification code.';
|
||||
|
||||
@override
|
||||
String get startWithCameraOpen => 'Start with camera open';
|
||||
|
||||
|
|
@ -1612,6 +1664,40 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get verificationBadgeRedDesc =>
|
||||
'A contact whose identity has *not* yet been verified.';
|
||||
|
||||
@override
|
||||
String get manualVerificationOpen => 'Safety numbers';
|
||||
|
||||
@override
|
||||
String get manualVerificationTitle => 'Compare safety number';
|
||||
|
||||
@override
|
||||
String get manualVerificationMarkVerified => 'Mark as verified';
|
||||
|
||||
@override
|
||||
String manualVerificationDescription(String username) {
|
||||
return 'Compare this safety number with $username. If both devices show the same number, your end-to-end encryption is verified.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get manualVerificationMissingKeyTitle => 'No safety number available';
|
||||
|
||||
@override
|
||||
String manualVerificationMissingKeyDescription(String username) {
|
||||
return 'There is no Signal session with $username yet. Exchange a message first, then try again.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get manualVerificationLoadFailed =>
|
||||
'The safety number could not be loaded.';
|
||||
|
||||
@override
|
||||
String get manualVerificationRetry => 'Try again';
|
||||
|
||||
@override
|
||||
String manualVerificationSuccess(String username) {
|
||||
return 'You compared the safety number with $username and confirmed their identity.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get scanNow => 'Scan now';
|
||||
|
||||
|
|
|
|||
|
|
@ -174,11 +174,32 @@
|
|||
}
|
||||
},
|
||||
"verificationTypeLink": "Per Link verifiziert.",
|
||||
"verificationTypeManual": "Sicherheitsnummer manuell verglichen.",
|
||||
"verificationTypeMigratedFromOldVersion": "Von alter Version migriert",
|
||||
"contactViewMessage": "Nachricht",
|
||||
"contactNickname": "Spitzname",
|
||||
"contactNicknameNew": "Neuer Spitzname",
|
||||
"contactBlock": "Blockieren",
|
||||
"contactTroubleshoot": "Verbindungsprobleme beheben",
|
||||
"contactResetSessionTitle": "Verbindung zu {username} zurücksetzen?",
|
||||
"@contactResetSessionTitle": {
|
||||
"placeholders": {
|
||||
"username": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contactResetSessionBody": "twonly löscht die aktuelle verschlüsselte Verbindung und baut sie anschließend neu auf. Nachrichten, die gerade übertragen werden, können dabei verloren gehen.",
|
||||
"contactResetSessionConfirm": "Verbindung zurücksetzen",
|
||||
"contactResetSessionSuccess": "Die verschlüsselte Verbindung zu {username} wurde neu aufgebaut.",
|
||||
"@contactResetSessionSuccess": {
|
||||
"placeholders": {
|
||||
"username": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contactResetSessionFailed": "Die Verbindung konnte nicht zurückgesetzt werden. Prüfe deine Internetverbindung und versuche es erneut.",
|
||||
"contactRemove": "Kontakt löschen",
|
||||
"contactRemoveTitle": "{username} löschen?",
|
||||
"contactRemoveBody": "Den Kontakt dauerhaft entfernen. Wenn der Kontakt versucht, dir eine neue Nachricht zu senden, musst du den Kontakt erst wieder akzeptieren.",
|
||||
|
|
@ -487,6 +508,13 @@
|
|||
"gotLinkFromFriend": "Ja, der Link kommt direkt von der Person.",
|
||||
"couldNotVerifyUsername": "{username} konnte nicht verifiziert werden",
|
||||
"linkPubkeyDoesNotMatch": "Der öffentliche Schlüssel im Link stimmt nicht mit dem für diesen Kontakt gespeicherten öffentlichen Schlüssel überein. Triff die Person persönlich und scanne den QR-Code direkt!",
|
||||
"verificationMissingSession": "Für diesen Kontakt ist noch keine sichere Sitzung gespeichert. Tauscht zuerst eine Nachricht aus und lass dir danach einen neuen Verifizierungslink senden.",
|
||||
"verificationLinkInvalidTitle": "Ungültiger Verifizierungslink",
|
||||
"verificationLinkInvalid": "Der Verifizierungslink ist unvollständig oder ungültig. Bitte die Person, ihn erneut aus ihrem twonly-Profil zu teilen.",
|
||||
"verificationLinkErrorTitle": "Verifizierung nicht möglich",
|
||||
"verificationLinkError": "twonly konnte diesen Verifizierungslink nicht prüfen. Versuche es bitte erneut.",
|
||||
"verificationOwnProfileTitle": "Das ist dein Profil",
|
||||
"verificationOwnProfile": "Du hast deinen eigenen Verifizierungscode gescannt.",
|
||||
"startWithCameraOpen": "Mit geöffneter Kamera starten",
|
||||
"showImagePreviewWhenSending": "Bildvorschau bei der Auswahl von Empfängern anzeigen",
|
||||
"verifiedPublicKey": "Die Identität von {username} wurde erfolgreich überprüft.",
|
||||
|
|
@ -518,6 +546,36 @@
|
|||
"verificationBadgeGreenDesc": "Ein Kontakt, den du über den QR-code *persönlich verifiziert* hast.",
|
||||
"verificationBadgeYellowDesc": "Ein Kontakt, der von mind. einem *deiner Kontakte verifiziert* wurde.",
|
||||
"verificationBadgeRedDesc": "Ein Kontakt, dessen Identität noch *nicht überprüft* wurde.",
|
||||
"manualVerificationOpen": "Sicherheitsnummern",
|
||||
"manualVerificationTitle": "Sicherheitsnummer vergleichen",
|
||||
"manualVerificationMarkVerified": "Als verifiziert markieren",
|
||||
"manualVerificationDescription": "Vergleiche diese Sicherheitsnummer mit {username}. Wenn auf beiden Geräten dieselbe Nummer angezeigt wird, ist eure Ende-zu-Ende-Verschlüsselung verifiziert.",
|
||||
"@manualVerificationDescription": {
|
||||
"placeholders": {
|
||||
"username": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"manualVerificationMissingKeyTitle": "Keine Sicherheitsnummer verfügbar",
|
||||
"manualVerificationMissingKeyDescription": "Mit {username} besteht noch keine Signal-Sitzung. Tauscht zuerst eine Nachricht aus und versucht es danach erneut.",
|
||||
"@manualVerificationMissingKeyDescription": {
|
||||
"placeholders": {
|
||||
"username": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"manualVerificationLoadFailed": "Die Sicherheitsnummer konnte nicht geladen werden.",
|
||||
"manualVerificationRetry": "Erneut versuchen",
|
||||
"manualVerificationSuccess": "Du hast die Sicherheitsnummer mit {username} verglichen und die Identität bestätigt.",
|
||||
"@manualVerificationSuccess": {
|
||||
"placeholders": {
|
||||
"username": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanNow": "Jetzt scannen",
|
||||
"qrScannerVerifyHint": "Zum Verifizieren muss die andere Person ihren QR-Code öffnen (Chat-Liste > QR-Code-Button unten rechts)",
|
||||
"qrScannerVerifyUserHint": "Zum Verifizieren muss {username} seinen QR-Code öffnen (Chat-Liste > QR-Code-Button unten rechts)",
|
||||
|
|
|
|||
|
|
@ -170,11 +170,32 @@
|
|||
}
|
||||
},
|
||||
"verificationTypeLink": "Verified via link.",
|
||||
"verificationTypeManual": "Safety number compared manually.",
|
||||
"verificationTypeMigratedFromOldVersion": "Migrated from old version.",
|
||||
"contactViewMessage": "Message",
|
||||
"contactNickname": "Nickname",
|
||||
"contactNicknameNew": "New nickname",
|
||||
"contactBlock": "Block",
|
||||
"contactTroubleshoot": "Troubleshoot connection",
|
||||
"contactResetSessionTitle": "Reset connection with {username}?",
|
||||
"@contactResetSessionTitle": {
|
||||
"placeholders": {
|
||||
"username": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contactResetSessionBody": "twonly will remove the current encrypted connection and establish a new one. Messages currently in transit may be lost.",
|
||||
"contactResetSessionConfirm": "Reset connection",
|
||||
"contactResetSessionSuccess": "The encrypted connection with {username} was re-established.",
|
||||
"@contactResetSessionSuccess": {
|
||||
"placeholders": {
|
||||
"username": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contactResetSessionFailed": "The connection could not be reset. Check your internet connection and try again.",
|
||||
"contactBlockTitle": "Block {username}",
|
||||
"@contactBlockTitle": {
|
||||
"placeholders": {
|
||||
|
|
@ -497,6 +518,13 @@
|
|||
"gotLinkFromFriend": "Yes, I got the link from my friend!",
|
||||
"couldNotVerifyUsername": "Could not verify {username}",
|
||||
"linkPubkeyDoesNotMatch": "The public key in the link does not match the public key stored for this contact. Try to meet your friend in person and scan the QR code directly!",
|
||||
"verificationMissingSession": "There is no secure session stored for this contact yet. Exchange a message first, then ask the person to share a new verification link.",
|
||||
"verificationLinkInvalidTitle": "Invalid verification link",
|
||||
"verificationLinkInvalid": "The verification link is incomplete or invalid. Ask the person to share it again from their twonly profile.",
|
||||
"verificationLinkErrorTitle": "Verification unavailable",
|
||||
"verificationLinkError": "twonly could not check this verification link. Please try again.",
|
||||
"verificationOwnProfileTitle": "This is your profile",
|
||||
"verificationOwnProfile": "You scanned your own verification code.",
|
||||
"startWithCameraOpen": "Start with camera open",
|
||||
"showImagePreviewWhenSending": "Display image preview when selecting recipients",
|
||||
"verifiedPublicKey": "The identity of {username} has been successfully verified.",
|
||||
|
|
@ -528,6 +556,36 @@
|
|||
"verificationBadgeGreenDesc": "A contact you have *personally verified* using the QR code.",
|
||||
"verificationBadgeYellowDesc": "A contact who has been verified by at least one of *your contacts*.",
|
||||
"verificationBadgeRedDesc": "A contact whose identity has *not* yet been verified.",
|
||||
"manualVerificationOpen": "Safety numbers",
|
||||
"manualVerificationTitle": "Compare safety number",
|
||||
"manualVerificationMarkVerified": "Mark as verified",
|
||||
"manualVerificationDescription": "Compare this safety number with {username}. If both devices show the same number, your end-to-end encryption is verified.",
|
||||
"@manualVerificationDescription": {
|
||||
"placeholders": {
|
||||
"username": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"manualVerificationMissingKeyTitle": "No safety number available",
|
||||
"manualVerificationMissingKeyDescription": "There is no Signal session with {username} yet. Exchange a message first, then try again.",
|
||||
"@manualVerificationMissingKeyDescription": {
|
||||
"placeholders": {
|
||||
"username": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"manualVerificationLoadFailed": "The safety number could not be loaded.",
|
||||
"manualVerificationRetry": "Try again",
|
||||
"manualVerificationSuccess": "You compared the safety number with {username} and confirmed their identity.",
|
||||
"@manualVerificationSuccess": {
|
||||
"placeholders": {
|
||||
"username": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanNow": "Scan now",
|
||||
"qrScannerVerifyHint": "To verify, the other person must open their QR code (Chat Lists > QR code button at the bottom right)",
|
||||
"qrScannerVerifyUserHint": "To verify, {username} must open their QR code (Chat Lists > QR code button at the bottom right)",
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import 'package:twonly/src/visual/views/settings/help/diagnostics.view.dart';
|
|||
import 'package:twonly/src/visual/views/settings/help/faq.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/help/faq/verification_badge_faq.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/help/help.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/help/manual_verification.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/help/news.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/notification.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/privacy.view.dart';
|
||||
|
|
@ -255,6 +256,14 @@ final routerProvider = GoRouter(
|
|||
builder: (context, state) => VerificationBadeFaqView(
|
||||
contact: state.extra as Contact?,
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'manual',
|
||||
builder: (context, state) => ManualVerificationView(
|
||||
contact: state.extra! as Contact,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -9,20 +9,27 @@ import 'package:go_router/go_router.dart';
|
|||
import 'package:twonly/core/bridge/wrapper/signal.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/services/passwordless_recovery.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/qr.utils.dart';
|
||||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||
import 'package:twonly/src/visual/components/verification_failure_dialog.comp.dart';
|
||||
import 'package:twonly/src/visual/components/verification_success_dialog.comp.dart';
|
||||
import 'package:twonly/src/visual/views/contact/add_contact_via_qr_link.view.dart';
|
||||
import 'package:twonly/src/visual/views/contact/add_new_contact.view.dart';
|
||||
|
||||
Future<bool> handleIntentUrl(BuildContext context, Uri uri) async {
|
||||
if (!uri.scheme.startsWith('http')) return false;
|
||||
if (uri.scheme != 'http' && uri.scheme != 'https') return false;
|
||||
if (uri.host != 'me.twonly.eu') return false;
|
||||
if (uri.hasEmptyPath) return false;
|
||||
if (uri.hasEmptyPath) {
|
||||
if (context.mounted) await _showInvalidVerificationLink(context);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if this is the QR code link which was
|
||||
// therefore scanned with the system camera
|
||||
|
|
@ -37,37 +44,47 @@ Future<bool> handleIntentUrl(BuildContext context, Uri uri) async {
|
|||
|
||||
if (!context.mounted) return false;
|
||||
|
||||
if (result != null) {
|
||||
final (profile, contact, verificationOk) = result;
|
||||
|
||||
if (profile.username == userService.currentUser.username) {
|
||||
await context.push(Routes.settingsPublicProfile);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (contact != null) {
|
||||
if (verificationOk) {
|
||||
await context.push(Routes.profileContact(contact.userId));
|
||||
} else {
|
||||
await _pubKeysDoNotMatch(context, contact.username);
|
||||
}
|
||||
} else {
|
||||
switch (result.status) {
|
||||
case QrCodeLinkStatus.newContact:
|
||||
await context.navPush(
|
||||
AddContactViaQrLinkView(
|
||||
profile: profile,
|
||||
qrCodeLink: uri.toString(),
|
||||
profile: result.profile!,
|
||||
),
|
||||
);
|
||||
case QrCodeLinkStatus.verified:
|
||||
final contact = result.contact!;
|
||||
await VerificationSuccessDialog.show(context, contact);
|
||||
if (context.mounted) {
|
||||
await context.push(Routes.profileContact(contact.userId));
|
||||
}
|
||||
case QrCodeLinkStatus.ownProfile:
|
||||
case QrCodeLinkStatus.invalid:
|
||||
case QrCodeLinkStatus.keyMismatch:
|
||||
case QrCodeLinkStatus.missingSession:
|
||||
case QrCodeLinkStatus.error:
|
||||
await showQrCodeVerificationFailure(context, result);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
final publicKey = uri.hasFragment ? uri.fragment : null;
|
||||
final userPaths = uri.path.split('/');
|
||||
if (userPaths.length != 2) return false;
|
||||
final username = userPaths[1];
|
||||
final rawPathSegments = uri.pathSegments
|
||||
.where((part) => part.isNotEmpty)
|
||||
.toList();
|
||||
if (rawPathSegments.firstOrNull == 'qr' ||
|
||||
rawPathSegments.firstOrNull == 'r') {
|
||||
if (context.mounted) await _showInvalidVerificationLink(context);
|
||||
return true;
|
||||
}
|
||||
|
||||
final publicKey = uri.hasFragment && uri.fragment.isNotEmpty
|
||||
? uri.fragment
|
||||
: null;
|
||||
if (rawPathSegments.length != 1) {
|
||||
if (context.mounted) await _showInvalidVerificationLink(context);
|
||||
return true;
|
||||
}
|
||||
final username = rawPathSegments.single;
|
||||
|
||||
if (!context.mounted) return false;
|
||||
|
||||
|
|
@ -80,34 +97,74 @@ Future<bool> handleIntentUrl(BuildContext context, Uri uri) async {
|
|||
'Opened via deep link!: username = $username public_key = ${uri.fragment}',
|
||||
);
|
||||
|
||||
final contacts = await twonlyDB.contactsDao.getContactsByUsername(username);
|
||||
Uint8List? receivedPublicKey;
|
||||
if (publicKey != null) {
|
||||
try {
|
||||
receivedPublicKey = base64Url.decode(publicKey);
|
||||
} catch (e) {
|
||||
Log.warn('Invalid public key in verification link: $e');
|
||||
await _showInvalidVerificationLink(context);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
late final List<Contact> contacts;
|
||||
try {
|
||||
contacts = await twonlyDB.contactsDao.getContactsByUsername(username);
|
||||
} catch (e) {
|
||||
Log.warn('Could not load contact for verification link: $e');
|
||||
if (context.mounted) await _showVerificationLinkError(context);
|
||||
return true;
|
||||
}
|
||||
if (!context.mounted) return true;
|
||||
|
||||
if (contacts.isEmpty) {
|
||||
// User does not yet exists, making a request...
|
||||
Uint8List? publicKeyBytes;
|
||||
if (publicKey != null) {
|
||||
publicKeyBytes = base64Url.decode(publicKey);
|
||||
}
|
||||
await context.navPush(
|
||||
AddNewUserView(
|
||||
username: username,
|
||||
publicKey: publicKeyBytes,
|
||||
publicKey: receivedPublicKey,
|
||||
),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (publicKey != null) {
|
||||
try {
|
||||
final contact = contacts.first;
|
||||
if (publicKey == null) {
|
||||
await VerificationFailureDialog.show(
|
||||
context,
|
||||
title: context.lang.couldNotVerifyUsername(
|
||||
getContactDisplayName(contact),
|
||||
),
|
||||
message: context.lang.verificationLinkInvalid,
|
||||
contact: contact,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
final storedPublicKey = await RustSignal.getContactPublicKey(
|
||||
contactId: contact.userId,
|
||||
);
|
||||
final receivedPublicKey = base64Url.decode(publicKey);
|
||||
if (storedPublicKey == null ||
|
||||
receivedPublicKey.isEmpty ||
|
||||
!context.mounted) {
|
||||
if (!context.mounted) return true;
|
||||
if (storedPublicKey == null) {
|
||||
await showQrCodeVerificationFailure(
|
||||
context,
|
||||
QrCodeLinkResult(
|
||||
status: QrCodeLinkStatus.missingSession,
|
||||
contact: contact,
|
||||
),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
if (receivedPublicKey == null || receivedPublicKey.isEmpty) {
|
||||
await showQrCodeVerificationFailure(
|
||||
context,
|
||||
QrCodeLinkResult(
|
||||
status: QrCodeLinkStatus.invalid,
|
||||
contact: contact,
|
||||
),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
if (storedPublicKey.equals(receivedPublicKey)) {
|
||||
|
|
@ -127,39 +184,60 @@ Future<bool> handleIntentUrl(BuildContext context, Uri uri) async {
|
|||
await context.push(Routes.profileContact(contact.userId));
|
||||
}
|
||||
} else {
|
||||
await _pubKeysDoNotMatch(context, contact.username);
|
||||
await showQrCodeVerificationFailure(
|
||||
context,
|
||||
QrCodeLinkResult(
|
||||
status: QrCodeLinkStatus.keyMismatch,
|
||||
contact: contact,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.warn(e);
|
||||
if (context.mounted) {
|
||||
await showQrCodeVerificationFailure(
|
||||
context,
|
||||
QrCodeLinkResult(
|
||||
status: QrCodeLinkStatus.error,
|
||||
contact: contact,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> _pubKeysDoNotMatch(BuildContext context, String username) async {
|
||||
await showAlertDialog(
|
||||
Future<void> _showInvalidVerificationLink(BuildContext context) {
|
||||
return VerificationFailureDialog.show(
|
||||
context,
|
||||
context.lang.couldNotVerifyUsername(username),
|
||||
context.lang.linkPubkeyDoesNotMatch,
|
||||
customCancel: '',
|
||||
title: context.lang.verificationLinkInvalidTitle,
|
||||
message: context.lang.verificationLinkInvalid,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showVerificationLinkError(BuildContext context) {
|
||||
return VerificationFailureDialog.show(
|
||||
context,
|
||||
title: context.lang.verificationLinkErrorTitle,
|
||||
message: context.lang.verificationLinkError,
|
||||
);
|
||||
}
|
||||
|
||||
StreamSubscription<List<SharedFile>> initIntentStreams(
|
||||
BuildContext context,
|
||||
void Function(Uri) onUrlCallBack,
|
||||
Future<void> Function(Uri) onUrlCallBack,
|
||||
void Function(String, MediaType) onMediaCallBack,
|
||||
) {
|
||||
FlutterSharingIntent.instance.getInitialSharing().then((f) {
|
||||
FlutterSharingIntent.instance.getInitialSharing().then((f) async {
|
||||
if (!context.mounted) return;
|
||||
handleIntentSharedFile(context, f, onUrlCallBack, onMediaCallBack);
|
||||
await handleIntentSharedFile(context, f, onUrlCallBack, onMediaCallBack);
|
||||
});
|
||||
|
||||
return FlutterSharingIntent.instance.getMediaStream().listen(
|
||||
(f) {
|
||||
(f) async {
|
||||
if (!context.mounted) return;
|
||||
handleIntentSharedFile(context, f, onUrlCallBack, onMediaCallBack);
|
||||
await handleIntentSharedFile(context, f, onUrlCallBack, onMediaCallBack);
|
||||
},
|
||||
// ignore: inference_failure_on_untyped_parameter
|
||||
onError: (err) {
|
||||
|
|
@ -171,7 +249,7 @@ StreamSubscription<List<SharedFile>> initIntentStreams(
|
|||
Future<void> handleIntentSharedFile(
|
||||
BuildContext context,
|
||||
List<SharedFile> files,
|
||||
void Function(Uri) onUrlCallBack,
|
||||
Future<void> Function(Uri) onUrlCallBack,
|
||||
void Function(String, MediaType) onMediaCallBack,
|
||||
) async {
|
||||
for (final file in files) {
|
||||
|
|
@ -189,7 +267,7 @@ Future<void> handleIntentSharedFile(
|
|||
if (file.value?.startsWith('http') ?? false) {
|
||||
final uri = Uri.parse(file.value!);
|
||||
Log.info('Got link via handle intent share file: ${uri.scheme}');
|
||||
onUrlCallBack(uri);
|
||||
await onUrlCallBack(uri);
|
||||
}
|
||||
case SharedMediaType.IMAGE:
|
||||
var type = MediaType.image;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,28 @@ import 'package:twonly/src/model/protobuf/client/generated/qr.pb.dart';
|
|||
import 'package:twonly/src/services/key_verification.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
enum QrCodeLinkStatus {
|
||||
invalid,
|
||||
ownProfile,
|
||||
newContact,
|
||||
verified,
|
||||
keyMismatch,
|
||||
missingSession,
|
||||
error,
|
||||
}
|
||||
|
||||
class QrCodeLinkResult {
|
||||
const QrCodeLinkResult({
|
||||
required this.status,
|
||||
this.profile,
|
||||
this.contact,
|
||||
});
|
||||
|
||||
final QrCodeLinkStatus status;
|
||||
final PublicProfile? profile;
|
||||
final Contact? contact;
|
||||
}
|
||||
|
||||
class QrCodeUtils {
|
||||
static String linkPrefix = 'https://me.twonly.eu/qr/#';
|
||||
|
||||
|
|
@ -45,8 +67,7 @@ class QrCodeUtils {
|
|||
return link;
|
||||
}
|
||||
|
||||
// returns: profile, NEW_USER=true/VERIFIED_USER=false, VERIFICATION_OK
|
||||
static Future<(PublicProfile, Contact?, bool)?> handleQrCodeLink(
|
||||
static Future<QrCodeLinkResult> handleQrCodeLink(
|
||||
String link,
|
||||
) async {
|
||||
late PublicProfile profile;
|
||||
|
|
@ -54,35 +75,53 @@ class QrCodeUtils {
|
|||
try {
|
||||
final bytes = base64Url.decode(link.replaceFirst(linkPrefix, ''));
|
||||
final envelope = QREnvelope.fromBuffer(bytes);
|
||||
if (envelope.type != QREnvelope_Type.PUBLIC_PROFILE) return null;
|
||||
if (envelope.type != QREnvelope_Type.PUBLIC_PROFILE) {
|
||||
return const QrCodeLinkResult(status: QrCodeLinkStatus.invalid);
|
||||
}
|
||||
profile = PublicProfile.fromBuffer(envelope.data);
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
return null;
|
||||
return const QrCodeLinkResult(status: QrCodeLinkStatus.invalid);
|
||||
}
|
||||
|
||||
try {
|
||||
final contact = await twonlyDB.contactsDao.getContactById(
|
||||
profile.userId.toInt(),
|
||||
);
|
||||
|
||||
if (contact == null) {
|
||||
if (profile.username == userService.currentUser.username) {
|
||||
return null;
|
||||
if (profile.userId.toInt() == userService.currentUser.userId) {
|
||||
return QrCodeLinkResult(
|
||||
status: QrCodeLinkStatus.ownProfile,
|
||||
profile: profile,
|
||||
);
|
||||
}
|
||||
// NEW_USER
|
||||
return (profile, null, false);
|
||||
|
||||
if (contact == null) {
|
||||
return QrCodeLinkResult(
|
||||
status: QrCodeLinkStatus.newContact,
|
||||
profile: profile,
|
||||
);
|
||||
}
|
||||
|
||||
final storedPublicKey = await RustSignal.getContactPublicKey(
|
||||
contactId: contact.userId,
|
||||
);
|
||||
if (storedPublicKey == null) return null;
|
||||
|
||||
final verificationOk = profile.publicIdentityKey.equals(
|
||||
storedPublicKey.toList(),
|
||||
if (storedPublicKey == null) {
|
||||
return QrCodeLinkResult(
|
||||
status: QrCodeLinkStatus.missingSession,
|
||||
profile: profile,
|
||||
contact: contact,
|
||||
);
|
||||
}
|
||||
|
||||
if (!profile.publicIdentityKey.equals(storedPublicKey.toList())) {
|
||||
return QrCodeLinkResult(
|
||||
status: QrCodeLinkStatus.keyMismatch,
|
||||
profile: profile,
|
||||
contact: contact,
|
||||
);
|
||||
}
|
||||
|
||||
if (verificationOk) {
|
||||
var useSecretVerificationToken = profile.hasSecretVerificationToken();
|
||||
if (profile.hasTimestamp()) {
|
||||
// Only notify the scanned user if the QR code was generated within the last 10 minutes.
|
||||
|
|
@ -107,9 +146,18 @@ class QrCodeUtils {
|
|||
contact.userId,
|
||||
VerificationType.qrScanned,
|
||||
);
|
||||
return QrCodeLinkResult(
|
||||
status: QrCodeLinkStatus.verified,
|
||||
profile: profile,
|
||||
contact: contact,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error('Failed to verify QR-code profile: $e');
|
||||
return QrCodeLinkResult(
|
||||
status: QrCodeLinkStatus.error,
|
||||
profile: profile,
|
||||
);
|
||||
}
|
||||
|
||||
return (profile, contact, verificationOk);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
140
lib/src/visual/components/verification_failure_dialog.comp.dart
Normal file
140
lib/src/visual/components/verification_failure_dialog.comp.dart
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/qr.utils.dart';
|
||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||
|
||||
class VerificationFailureDialog extends StatelessWidget {
|
||||
const VerificationFailureDialog({
|
||||
required this.title,
|
||||
required this.message,
|
||||
this.contact,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String message;
|
||||
final Contact? contact;
|
||||
|
||||
static Future<void> show(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required String message,
|
||||
Contact? contact,
|
||||
}) {
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => VerificationFailureDialog(
|
||||
title: title,
|
||||
message: message,
|
||||
contact: contact,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
backgroundColor: context.color.surface,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 28),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (contact != null) ...[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
AvatarIcon(contactId: contact!.userId, fontSize: 16),
|
||||
const SizedBox(width: 12),
|
||||
Flexible(
|
||||
child: Text(
|
||||
getContactDisplayName(contact!),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
Container(
|
||||
width: 96,
|
||||
height: 96,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: context.color.errorContainer,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.gpp_bad_outlined,
|
||||
size: 52,
|
||||
color: context.color.onErrorContainer,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
message,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: context.color.onSurface.withValues(alpha: 0.72),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: MyButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(context.lang.close),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showQrCodeVerificationFailure(
|
||||
BuildContext context,
|
||||
QrCodeLinkResult result,
|
||||
) {
|
||||
final contact = result.contact;
|
||||
final title = contact == null
|
||||
? switch (result.status) {
|
||||
QrCodeLinkStatus.invalid => context.lang.verificationLinkInvalidTitle,
|
||||
QrCodeLinkStatus.ownProfile =>
|
||||
context.lang.verificationOwnProfileTitle,
|
||||
_ => context.lang.verificationLinkErrorTitle,
|
||||
}
|
||||
: context.lang.couldNotVerifyUsername(getContactDisplayName(contact));
|
||||
|
||||
final message = switch (result.status) {
|
||||
QrCodeLinkStatus.keyMismatch => context.lang.linkPubkeyDoesNotMatch,
|
||||
QrCodeLinkStatus.missingSession => context.lang.verificationMissingSession,
|
||||
QrCodeLinkStatus.invalid => context.lang.verificationLinkInvalid,
|
||||
QrCodeLinkStatus.ownProfile => context.lang.verificationOwnProfile,
|
||||
_ => context.lang.verificationLinkError,
|
||||
};
|
||||
|
||||
return VerificationFailureDialog.show(
|
||||
context,
|
||||
title: title,
|
||||
message: message,
|
||||
contact: contact,
|
||||
);
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ import 'package:twonly/src/utils/qr.utils.dart';
|
|||
import 'package:twonly/src/visual/components/add_contact_dialog.comp.dart';
|
||||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||
import 'package:twonly/src/visual/components/snackbar.dart';
|
||||
import 'package:twonly/src/visual/components/verification_failure_dialog.comp.dart';
|
||||
import 'package:twonly/src/visual/components/verification_success_dialog.comp.dart';
|
||||
import 'package:twonly/src/visual/helpers/screenshot.helper.dart';
|
||||
import 'package:twonly/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart';
|
||||
|
|
@ -521,6 +522,7 @@ class MainCameraController {
|
|||
Future<void> _processBarcode(InputImage inputImage) async {
|
||||
if (_isBusy) return;
|
||||
_isBusy = true;
|
||||
try {
|
||||
final barcodes = await _barcodeScanner.processImage(inputImage);
|
||||
final controller = cameraController;
|
||||
if (inputImage.metadata?.size != null &&
|
||||
|
|
@ -555,9 +557,9 @@ class MainCameraController {
|
|||
if (_handledProfileLinks.contains(link)) continue;
|
||||
_handledProfileLinks.add(link);
|
||||
|
||||
final res = await QrCodeUtils.handleQrCodeLink(link);
|
||||
if (res == null) continue;
|
||||
final (profile, contact, verificationOk) = res;
|
||||
final result = await QrCodeUtils.handleQrCodeLink(link);
|
||||
final profile = result.profile;
|
||||
final contact = result.contact;
|
||||
|
||||
if (contact?.blocked ?? false) {
|
||||
await twonlyDB.contactsDao.updateContact(
|
||||
|
|
@ -566,9 +568,9 @@ class MainCameraController {
|
|||
);
|
||||
}
|
||||
|
||||
if (contact == null || contact.deletedByUser) {
|
||||
if (result.status == QrCodeLinkStatus.newContact) {
|
||||
final context = cameraPreviewKey.currentContext;
|
||||
if (context != null && context.mounted) {
|
||||
if (profile != null && context != null && context.mounted) {
|
||||
unawaited(HapticFeedback.heavyImpact());
|
||||
final shouldRequest = await AddContactDialog.show(
|
||||
context,
|
||||
|
|
@ -597,15 +599,24 @@ class MainCameraController {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (contactsVerified[contact.userId] == null) {
|
||||
if (result.status != QrCodeLinkStatus.verified) {
|
||||
final context = cameraPreviewKey.currentContext;
|
||||
if (context != null && context.mounted) {
|
||||
unawaited(HapticFeedback.heavyImpact());
|
||||
await showQrCodeVerificationFailure(context, result);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (contact != null && contactsVerified[contact.userId] == null) {
|
||||
contactsVerified[contact.userId] = ScannedVerifiedContact(
|
||||
contact: contact,
|
||||
verificationOk: verificationOk,
|
||||
verificationOk: true,
|
||||
);
|
||||
|
||||
unawaited(HapticFeedback.heavyImpact());
|
||||
final context = cameraPreviewKey.currentContext;
|
||||
if (verificationOk && context != null && context.mounted) {
|
||||
if (context != null && context.mounted) {
|
||||
await VerificationSuccessDialog.show(context, contact);
|
||||
onVerificationSuccessDismissed?.call(contact);
|
||||
}
|
||||
|
|
@ -625,9 +636,13 @@ class MainCameraController {
|
|||
}
|
||||
}
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
Log.error('QR-code processing failed: $e\n$stackTrace');
|
||||
} finally {
|
||||
_isBusy = false;
|
||||
setState?.call();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _processFaces(InputImage inputImage) async {
|
||||
if (_isBusyFaces) return;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,6 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/drift.dart' hide Column;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/qr.pb.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
|
@ -15,12 +10,10 @@ import 'package:twonly/src/visual/components/snackbar.dart';
|
|||
class AddContactViaQrLinkView extends StatefulWidget {
|
||||
const AddContactViaQrLinkView({
|
||||
required this.profile,
|
||||
this.qrCodeLink,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final PublicProfile profile;
|
||||
final String? qrCodeLink;
|
||||
|
||||
@override
|
||||
State<AddContactViaQrLinkView> createState() =>
|
||||
|
|
@ -36,31 +29,15 @@ class _AddContactViaQrLinkViewState extends State<AddContactViaQrLinkView> {
|
|||
});
|
||||
|
||||
try {
|
||||
final userData = FrbUserData(
|
||||
userId: widget.profile.userId.toInt(),
|
||||
publicIdentityKey: Uint8List.fromList(widget.profile.publicIdentityKey),
|
||||
username: Uint8List.fromList(utf8.encode(widget.profile.username)),
|
||||
final success = await addNewContactFromPublicProfile(widget.profile);
|
||||
if (!success) {
|
||||
if (mounted) {
|
||||
showSnackbar(
|
||||
context,
|
||||
context.lang.additionalUserAddError(widget.profile.username),
|
||||
);
|
||||
|
||||
final added = await twonlyDB.contactsDao.insertOnConflictUpdate(
|
||||
ContactsCompanion(
|
||||
username: Value(widget.profile.username),
|
||||
userId: Value(widget.profile.userId.toInt()),
|
||||
requested: const Value(false),
|
||||
blocked: const Value(false),
|
||||
deletedByUser: const Value(false),
|
||||
),
|
||||
);
|
||||
|
||||
if (added > 0) {
|
||||
await RustApi.tryRequestContactById(
|
||||
contactId: userData.userId,
|
||||
expectedPublicKey: userData.publicIdentityKey,
|
||||
);
|
||||
if (widget.qrCodeLink != null) {
|
||||
// As the user does now exist he can now be marked as verified
|
||||
await QrCodeUtils.handleQrCodeLink(widget.qrCodeLink!);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:drift/drift.dart' hide Column;
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:twonly/core/bridge/wrapper/signal.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
|
|
@ -38,6 +39,7 @@ class ContactView extends StatefulWidget {
|
|||
class _ContactViewState extends State<ContactView> {
|
||||
Contact? _contact;
|
||||
List<GroupMember> _memberOfGroups = [];
|
||||
bool _isResettingSignalSession = false;
|
||||
|
||||
late StreamSubscription<Contact?> _streamContact;
|
||||
late StreamSubscription<List<GroupMember>> _streamMemberOfGroups;
|
||||
|
|
@ -138,6 +140,34 @@ class _ContactViewState extends State<ContactView> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> handleResetSignalSession(Contact contact) async {
|
||||
if (_isResettingSignalSession) return;
|
||||
|
||||
final displayName = getContactDisplayName(contact);
|
||||
final confirmed = await showAlertDialog(
|
||||
context,
|
||||
context.lang.contactResetSessionTitle(displayName),
|
||||
context.lang.contactResetSessionBody,
|
||||
customOk: context.lang.contactResetSessionConfirm,
|
||||
);
|
||||
if (!confirmed || !mounted) return;
|
||||
|
||||
setState(() => _isResettingSignalSession = true);
|
||||
final result = await rustApiResult(
|
||||
RustSignal.resetContactSession(contactId: contact.userId),
|
||||
);
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() => _isResettingSignalSession = false);
|
||||
showSnackbar(
|
||||
context,
|
||||
result.isSuccess
|
||||
? context.lang.contactResetSessionSuccess(displayName)
|
||||
: context.lang.contactResetSessionFailed,
|
||||
level: result.isSuccess ? SnackbarLevel.success : SnackbarLevel.error,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_contact == null) return Container();
|
||||
|
|
@ -285,6 +315,19 @@ class _ContactViewState extends State<ContactView> {
|
|||
text: context.lang.contactBlock,
|
||||
onTap: () => handleUserBlockRequest(contact),
|
||||
),
|
||||
BetterListTile(
|
||||
icon: FontAwesomeIcons.screwdriverWrench,
|
||||
text: context.lang.contactTroubleshoot,
|
||||
trailing: _isResettingSignalSession
|
||||
? const SizedBox.square(
|
||||
dimension: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: null,
|
||||
onTap: _isResettingSignalSession
|
||||
? null
|
||||
: () => handleResetSignalSession(contact),
|
||||
),
|
||||
BetterListTile(
|
||||
icon: FontAwesomeIcons.userMinus,
|
||||
iconSize: 16,
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ class _VerificationExpansionTileCompState
|
|||
getContactDisplayName(widget.contact),
|
||||
),
|
||||
VerificationType.link => context.lang.verificationTypeLink,
|
||||
VerificationType.manual => context.lang.verificationTypeManual,
|
||||
VerificationType.contactSharedByVerified =>
|
||||
verifier != null
|
||||
? context.lang.contactVerifiedBy(getContactDisplayName(verifier))
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/components/verification_badge_info.comp.dart';
|
||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||
|
||||
class VerificationBadeFaqView extends StatefulWidget {
|
||||
const VerificationBadeFaqView({super.key, this.contact});
|
||||
|
|
@ -27,6 +31,31 @@ class _VerificationBadeFaqViewState extends State<VerificationBadeFaqView> {
|
|||
displayButtons: true,
|
||||
contact: widget.contact,
|
||||
),
|
||||
if (widget.contact != null) ...[
|
||||
const SizedBox(height: 24),
|
||||
Center(
|
||||
child: IntrinsicWidth(
|
||||
child: MyButton(
|
||||
variant: MyButtonVariant.secondaryMiddle,
|
||||
onPressed: () => context.push(
|
||||
Routes.settingsHelpFaqManualVerification,
|
||||
extra: widget.contact,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const FaIcon(FontAwesomeIcons.fingerprint, size: 20),
|
||||
const SizedBox(width: 10),
|
||||
Flexible(
|
||||
child: Text(context.lang.manualVerificationOpen),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
274
lib/src/visual/views/settings/help/manual_verification.view.dart
Normal file
274
lib/src/visual/views/settings/help/manual_verification.view.dart
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:twonly/core/bridge/wrapper/signal.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/components/verification_success_dialog.comp.dart';
|
||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||
|
||||
class ManualVerificationView extends StatefulWidget {
|
||||
const ManualVerificationView({required this.contact, super.key});
|
||||
|
||||
final Contact contact;
|
||||
|
||||
@override
|
||||
State<ManualVerificationView> createState() => _ManualVerificationViewState();
|
||||
}
|
||||
|
||||
class _ManualVerificationViewState extends State<ManualVerificationView> {
|
||||
late Future<String?> _safetyNumber;
|
||||
bool _isSaving = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadSafetyNumber();
|
||||
}
|
||||
|
||||
void _loadSafetyNumber() {
|
||||
_safetyNumber = RustSignal.getSafetyNumber(
|
||||
contactId: widget.contact.userId,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _markAsVerified() async {
|
||||
if (_isSaving) return;
|
||||
setState(() => _isSaving = true);
|
||||
|
||||
await twonlyDB.keyVerificationDao.addKeyVerification(
|
||||
widget.contact.userId,
|
||||
VerificationType.manual,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
final displayName = getContactDisplayName(widget.contact);
|
||||
await VerificationSuccessDialog.show(
|
||||
context,
|
||||
widget.contact,
|
||||
message: context.lang.manualVerificationSuccess(displayName),
|
||||
);
|
||||
if (mounted) context.pop();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final displayName = getContactDisplayName(widget.contact);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.lang.manualVerificationTitle),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: FutureBuilder<String?>(
|
||||
future: _safetyNumber,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (snapshot.hasError) {
|
||||
return _SafetyNumberError(
|
||||
message: context.lang.manualVerificationLoadFailed,
|
||||
onRetry: () => setState(_loadSafetyNumber),
|
||||
);
|
||||
}
|
||||
|
||||
final safetyNumber = snapshot.data;
|
||||
if (safetyNumber == null) {
|
||||
return _SafetyNumberError(
|
||||
title: context.lang.manualVerificationMissingKeyTitle,
|
||||
message: context.lang.manualVerificationMissingKeyDescription(
|
||||
displayName,
|
||||
),
|
||||
onRetry: () => setState(_loadSafetyNumber),
|
||||
);
|
||||
}
|
||||
|
||||
return _SafetyNumberContent(
|
||||
safetyNumber: safetyNumber,
|
||||
description: context.lang.manualVerificationDescription(
|
||||
displayName,
|
||||
),
|
||||
isSaving: _isSaving,
|
||||
onVerify: _markAsVerified,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SafetyNumberContent extends StatelessWidget {
|
||||
const _SafetyNumberContent({
|
||||
required this.safetyNumber,
|
||||
required this.description,
|
||||
required this.isSaving,
|
||||
required this.onVerify,
|
||||
});
|
||||
|
||||
final String safetyNumber;
|
||||
final String description;
|
||||
final bool isSaving;
|
||||
final VoidCallback onVerify;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final groups = <String>[
|
||||
for (var offset = 0; offset < safetyNumber.length; offset += 5)
|
||||
safetyNumber.substring(offset, offset + 5),
|
||||
];
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.fromLTRB(24, 28, 24, 40),
|
||||
children: [
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 520),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: context.color.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 32, 20, 20),
|
||||
child: Column(
|
||||
children: [
|
||||
FaIcon(
|
||||
FontAwesomeIcons.fingerprint,
|
||||
size: 42,
|
||||
color: context.color.onPrimaryContainer,
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
for (var row = 0; row < 3; row++) ...[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
for (final group in groups.skip(row * 4).take(4))
|
||||
Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
group,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: context.color.onPrimaryContainer,
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: 0.8,
|
||||
height: 1.45,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (row < 2) const SizedBox(height: 4),
|
||||
],
|
||||
const SizedBox(height: 28),
|
||||
IntrinsicWidth(
|
||||
child: MyButton(
|
||||
variant: MyButtonVariant.primaryMiddle,
|
||||
onPressed: isSaving ? null : onVerify,
|
||||
child: isSaving
|
||||
? const SizedBox.square(
|
||||
dimension: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
context.lang.manualVerificationMarkVerified,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 520),
|
||||
child: Text(
|
||||
description,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: context.color.onSurfaceVariant,
|
||||
fontSize: 14,
|
||||
height: 1.45,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SafetyNumberError extends StatelessWidget {
|
||||
const _SafetyNumberError({
|
||||
required this.message,
|
||||
required this.onRetry,
|
||||
this.title,
|
||||
});
|
||||
|
||||
final String? title;
|
||||
final String message;
|
||||
final VoidCallback onRetry;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.key_off_rounded,
|
||||
size: 54,
|
||||
color: context.color.onSurfaceVariant,
|
||||
),
|
||||
if (title != null) ...[
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
title!,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
message,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: context.color.onSurfaceVariant,
|
||||
fontSize: 16,
|
||||
height: 1.45,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
MyButton(
|
||||
variant: MyButtonVariant.secondaryMiddle,
|
||||
onPressed: onRetry,
|
||||
child: Text(context.lang.manualVerificationRetry),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,11 @@
|
|||
*/
|
||||
|
||||
use crate::bridge::get_twonly_flutter;
|
||||
use crate::context::Context;
|
||||
use crate::error::{Result, TwonlyError};
|
||||
use crate::services::contacts::ContactService;
|
||||
use crate::signal::engine::{FrbPqcPreKey, FrbPreKeyBundle};
|
||||
use crate::signal::reset::SessionResetLimiter;
|
||||
|
||||
pub struct RustSignal {}
|
||||
|
||||
|
|
@ -59,4 +62,34 @@ impl RustSignal {
|
|||
.get_contact_identity_key(&contact_id.to_string())
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_safety_number(contact_id: i64) -> Result<Option<String>> {
|
||||
let guard = get_twonly_flutter()?.signal_engine.lock().await;
|
||||
let engine = guard.as_ref().ok_or(TwonlyError::Initialization)?;
|
||||
engine.get_safety_number(contact_id).await
|
||||
}
|
||||
|
||||
/// Removes the current session with a contact and immediately establishes
|
||||
/// a fresh one while requiring the contact's stored identity key to match.
|
||||
pub async fn reset_contact_session(contact_id: i64) -> Result<()> {
|
||||
let ctx = Context::get_static()?;
|
||||
let expected_public_key = {
|
||||
let guard = ctx.signal_engine.lock().await;
|
||||
let engine = guard.as_ref().ok_or(TwonlyError::Initialization)?;
|
||||
let public_key = engine
|
||||
.get_contact_identity_key(&contact_id.to_string())
|
||||
.await?;
|
||||
engine
|
||||
.reset_session(&contact_id.to_string(), 1, true)
|
||||
.await?;
|
||||
public_key
|
||||
};
|
||||
|
||||
ContactService::new(ctx)
|
||||
.establish_signal_session(contact_id, expected_public_key)
|
||||
.await?;
|
||||
|
||||
let database = ctx.rust_db.read().await.clone();
|
||||
SessionResetLimiter::clear(&database.pool, &contact_id.to_string(), 1).await
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
|
|||
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
||||
);
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -2068471902;
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1675944024;
|
||||
|
||||
// Section: executor
|
||||
|
||||
|
|
@ -5735,6 +5735,46 @@ fn wire__crate__bridge__wrapper__signal__rust_signal_get_contact_public_key_impl
|
|||
},
|
||||
)
|
||||
}
|
||||
fn wire__crate__bridge__wrapper__signal__rust_signal_get_safety_number_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_signal_get_safety_number",
|
||||
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_contact_id = <i64>::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::signal::RustSignal::get_safety_number(
|
||||
api_contact_id,
|
||||
)
|
||||
.await?;
|
||||
Ok(output_ok)
|
||||
})()
|
||||
.await,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
fn wire__crate__bridge__wrapper__signal__rust_signal_get_user_public_key_impl(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
|
|
@ -5817,6 +5857,46 @@ fn wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(
|
|||
},
|
||||
)
|
||||
}
|
||||
fn wire__crate__bridge__wrapper__signal__rust_signal_reset_contact_session_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_signal_reset_contact_session",
|
||||
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_contact_id = <i64>::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::signal::RustSignal::reset_contact_session(
|
||||
api_contact_id,
|
||||
)
|
||||
.await?;
|
||||
Ok(output_ok)
|
||||
})()
|
||||
.await,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
fn wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
|
|
@ -7982,20 +8062,22 @@ fn pde_ffi_dispatcher_primary_impl(
|
|||
156 => wire__crate__bridge__wrapper__signal__rust_signal_generate_bundle_impl(port, ptr, rust_vec_len, data_len),
|
||||
157 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(port, ptr, rust_vec_len, data_len),
|
||||
158 => wire__crate__bridge__wrapper__signal__rust_signal_get_contact_public_key_impl(port, ptr, rust_vec_len, data_len),
|
||||
159 => wire__crate__bridge__wrapper__signal__rust_signal_get_user_public_key_impl(port, ptr, rust_vec_len, data_len),
|
||||
160 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len),
|
||||
161 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len),
|
||||
162 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len),
|
||||
163 => wire__crate__bridge__webxdc__send_update_impl(port, ptr, rust_vec_len, data_len),
|
||||
164 => wire__crate__bridge__webxdc__serve_impl(port, ptr, rust_vec_len, data_len),
|
||||
165 => wire__crate__bridge__groups__update_chat_deletion_time_impl(port, ptr, rust_vec_len, data_len),
|
||||
166 => wire__crate__bridge__groups__update_group_name_impl(port, ptr, rust_vec_len, data_len),
|
||||
167 => wire__crate__bridge__webxdc__updates_after_impl(port, ptr, rust_vec_len, data_len),
|
||||
169 => wire__crate__bridge__user_config__user_config_api_create_impl(port, ptr, rust_vec_len, data_len),
|
||||
170 => wire__crate__bridge__user_config__user_config_api_import_json_impl(port, ptr, rust_vec_len, data_len),
|
||||
171 => wire__crate__bridge__user_config__user_config_api_load_impl(port, ptr, rust_vec_len, data_len),
|
||||
172 => wire__crate__bridge__user_config__user_config_api_save_impl(port, ptr, rust_vec_len, data_len),
|
||||
173 => wire__crate__bridge__user_config__user_config_api_update_impl(port, ptr, rust_vec_len, data_len),
|
||||
159 => wire__crate__bridge__wrapper__signal__rust_signal_get_safety_number_impl(port, ptr, rust_vec_len, data_len),
|
||||
160 => wire__crate__bridge__wrapper__signal__rust_signal_get_user_public_key_impl(port, ptr, rust_vec_len, data_len),
|
||||
161 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len),
|
||||
162 => wire__crate__bridge__wrapper__signal__rust_signal_reset_contact_session_impl(port, ptr, rust_vec_len, data_len),
|
||||
163 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len),
|
||||
164 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len),
|
||||
165 => wire__crate__bridge__webxdc__send_update_impl(port, ptr, rust_vec_len, data_len),
|
||||
166 => wire__crate__bridge__webxdc__serve_impl(port, ptr, rust_vec_len, data_len),
|
||||
167 => wire__crate__bridge__groups__update_chat_deletion_time_impl(port, ptr, rust_vec_len, data_len),
|
||||
168 => wire__crate__bridge__groups__update_group_name_impl(port, ptr, rust_vec_len, data_len),
|
||||
169 => wire__crate__bridge__webxdc__updates_after_impl(port, ptr, rust_vec_len, data_len),
|
||||
171 => wire__crate__bridge__user_config__user_config_api_create_impl(port, ptr, rust_vec_len, data_len),
|
||||
172 => wire__crate__bridge__user_config__user_config_api_import_json_impl(port, ptr, rust_vec_len, data_len),
|
||||
173 => wire__crate__bridge__user_config__user_config_api_load_impl(port, ptr, rust_vec_len, data_len),
|
||||
174 => wire__crate__bridge__user_config__user_config_api_save_impl(port, ptr, rust_vec_len, data_len),
|
||||
175 => wire__crate__bridge__user_config__user_config_api_update_impl(port, ptr, rust_vec_len, data_len),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
|
@ -8013,12 +8095,12 @@ fn pde_ffi_dispatcher_sync_impl(
|
|||
51 => {
|
||||
wire__crate__bridge__api__rust_api_decode_avatar_svg_impl(ptr, rust_vec_len, data_len)
|
||||
}
|
||||
168 => wire__crate__bridge__user_config__user_config_api_clone_impl(
|
||||
170 => wire__crate__bridge__user_config__user_config_api_clone_impl(
|
||||
ptr,
|
||||
rust_vec_len,
|
||||
data_len,
|
||||
),
|
||||
174 => wire__crate__bridge__logging__write_log_impl(ptr, rust_vec_len, data_len),
|
||||
176 => wire__crate__bridge__logging__write_log_impl(ptr, rust_vec_len, data_len),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ use crate::error::{Result, TwonlyError};
|
|||
use crate::user_config::UserConfig;
|
||||
use chrono::{Duration, Utc};
|
||||
use libsignal_protocol::{
|
||||
message_encrypt, process_prekey_bundle, CiphertextMessageType, DeviceId, GenericSignedPreKey,
|
||||
IdentityKey, IdentityKeyPair, IdentityKeyStore, KyberPreKeyId, KyberPreKeyStore, PreKeyBundle,
|
||||
PreKeyId, PreKeySignalMessage, PreKeyStore, ProtocolAddress, PublicKey, SessionStore,
|
||||
SignalMessage, SignedPreKeyId, SignedPreKeyStore, Timestamp,
|
||||
message_encrypt, process_prekey_bundle, CiphertextMessageType, DeviceId, Fingerprint,
|
||||
GenericSignedPreKey, IdentityKey, IdentityKeyPair, IdentityKeyStore, KyberPreKeyId,
|
||||
KyberPreKeyStore, PreKeyBundle, PreKeyId, PreKeySignalMessage, PreKeyStore, ProtocolAddress,
|
||||
PublicKey, SessionStore, SignalMessage, SignedPreKeyId, SignedPreKeyStore, Timestamp,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
|
@ -50,6 +50,29 @@ pub struct FrbPqcPreKey {
|
|||
pub kyber_pre_key_signature: Vec<u8>,
|
||||
}
|
||||
|
||||
fn create_safety_number(
|
||||
local_id: &str,
|
||||
local_key_bytes: &[u8],
|
||||
remote_id: &str,
|
||||
remote_key_bytes: &[u8],
|
||||
) -> Result<String> {
|
||||
let local_key = IdentityKey::decode(local_key_bytes)
|
||||
.map_err(|error| TwonlyError::Signal(error.to_string()))?;
|
||||
let remote_key = IdentityKey::decode(remote_key_bytes)
|
||||
.map_err(|error| TwonlyError::Signal(error.to_string()))?;
|
||||
|
||||
Fingerprint::new(
|
||||
2,
|
||||
5_200,
|
||||
local_id.as_bytes(),
|
||||
&local_key,
|
||||
remote_id.as_bytes(),
|
||||
&remote_key,
|
||||
)
|
||||
.and_then(|fingerprint| fingerprint.display_string())
|
||||
.map_err(|error| TwonlyError::Signal(error.to_string()))
|
||||
}
|
||||
|
||||
impl RustSignalEngine {
|
||||
pub async fn on_connected(&self, ctx: &Arc<crate::context::Context>) -> Result<()> {
|
||||
let base = UserConfig::load_required_from(ctx)?;
|
||||
|
|
@ -165,6 +188,26 @@ impl RustSignalEngine {
|
|||
Ok(identity)
|
||||
}
|
||||
|
||||
pub async fn get_safety_number(&self, contact_id: i64) -> Result<Option<String>> {
|
||||
let Some(contact_key_bytes) = self
|
||||
.get_contact_identity_key(&contact_id.to_string())
|
||||
.await?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// Signal's standard 60-digit numeric fingerprint. Both participants
|
||||
// derive the same display string because libsignal sorts the two
|
||||
// independently generated fingerprint halves before displaying them.
|
||||
create_safety_number(
|
||||
&self.local_name,
|
||||
&self.get_identity_key().await?,
|
||||
&contact_id.to_string(),
|
||||
&contact_key_bytes,
|
||||
)
|
||||
.map(Some)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn generate_identity_key_pair() -> Result<Vec<u8>> {
|
||||
let mut csprng = rand::rngs::StdRng::from_os_rng();
|
||||
|
|
@ -678,6 +721,29 @@ mod tests {
|
|||
use rand::rngs::StdRng;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn safety_number_matches_signal_format_and_is_symmetric() {
|
||||
let alice_key =
|
||||
hex::decode("0506863bc66d02b40d27b8d49ca7c09e9239236f9d7d25d6fcca5ce13c7064d868")
|
||||
.unwrap();
|
||||
let bob_key =
|
||||
hex::decode("05f781b6fb32fed9ba1cf2de978d4d5da28dc34046ae814402b5c0dbd96fda907b")
|
||||
.unwrap();
|
||||
|
||||
let alice =
|
||||
create_safety_number("+14152222222", &alice_key, "+14153333333", &bob_key).unwrap();
|
||||
let bob =
|
||||
create_safety_number("+14153333333", &bob_key, "+14152222222", &alice_key).unwrap();
|
||||
|
||||
assert_eq!(alice, bob);
|
||||
assert_eq!(
|
||||
alice,
|
||||
"300354477692869396892869876765458257569162576843440918079131"
|
||||
);
|
||||
assert_eq!(alice.len(), 60);
|
||||
assert!(alice.bytes().all(|byte| byte.is_ascii_digit()));
|
||||
}
|
||||
|
||||
async fn create_test_engine(name: &str) -> (RustSignalEngine, tempfile::TempDir) {
|
||||
let dir = tempdir().unwrap();
|
||||
let db_path = dir.path().join(format!("{}.db", name));
|
||||
|
|
|
|||
|
|
@ -160,6 +160,16 @@ void main() {
|
|||
expect(await twonlyDB.keyVerificationDao.isContactVerified(10), true);
|
||||
});
|
||||
|
||||
test('isContactVerified returns true for manual comparison', () async {
|
||||
await insertContact(10);
|
||||
await addDirectVerification(10, VerificationType.manual);
|
||||
|
||||
final verifications = await twonlyDB.keyVerificationDao
|
||||
.getContactVerification(10);
|
||||
expect(verifications.single.type, VerificationType.manual);
|
||||
expect(await twonlyDB.keyVerificationDao.isContactVerified(10), true);
|
||||
});
|
||||
|
||||
test(
|
||||
'isContactVerified returns false when no verification exists',
|
||||
() async {
|
||||
|
|
|
|||
56
test/services/verification_link_handling_test.dart
Normal file
56
test/services/verification_link_handling_test.dart
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_sharing_intent/model/sharing_file.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:twonly/src/services/intent/links.intent.dart';
|
||||
import 'package:twonly/src/utils/qr.utils.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
test('invalid QR profile links return a visible failure status', () async {
|
||||
final result = await QrCodeUtils.handleQrCodeLink(
|
||||
'${QrCodeUtils.linkPrefix}not-a-valid-profile',
|
||||
);
|
||||
|
||||
expect(result.status, QrCodeLinkStatus.invalid);
|
||||
expect(result.profile, isNull);
|
||||
expect(result.contact, isNull);
|
||||
});
|
||||
|
||||
testWidgets('shared URLs are awaited by the common URL handler', (
|
||||
tester,
|
||||
) async {
|
||||
late BuildContext context;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Builder(
|
||||
builder: (builderContext) {
|
||||
context = builderContext;
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Uri? receivedUri;
|
||||
var callbackFinished = false;
|
||||
await handleIntentSharedFile(
|
||||
context,
|
||||
[
|
||||
SharedFile(
|
||||
value: 'https://me.twonly.eu/alice#public-key',
|
||||
type: SharedMediaType.URL,
|
||||
),
|
||||
],
|
||||
(uri) async {
|
||||
receivedUri = uri;
|
||||
await Future<void>.value();
|
||||
callbackFinished = true;
|
||||
},
|
||||
(_, _) {},
|
||||
);
|
||||
|
||||
expect(receivedUri, Uri.parse('https://me.twonly.eu/alice#public-key'));
|
||||
expect(callbackFinished, isTrue);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue