mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-09-01 08:54:08 +00:00
sealed sender
This commit is contained in:
parent
d89952c295
commit
da751cbded
47 changed files with 1779 additions and 76 deletions
|
|
@ -5970,8 +5970,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
UserConfig dco_decode_user_config(dynamic raw) {
|
UserConfig dco_decode_user_config(dynamic raw) {
|
||||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
final arr = raw as List<dynamic>;
|
final arr = raw as List<dynamic>;
|
||||||
if (arr.length != 59)
|
if (arr.length != 60)
|
||||||
throw Exception('unexpected arr length: expect 59 but see ${arr.length}');
|
throw Exception('unexpected arr length: expect 60 but see ${arr.length}');
|
||||||
return UserConfig(
|
return UserConfig(
|
||||||
userId: dco_decode_i_64(arr[0]),
|
userId: dco_decode_i_64(arr[0]),
|
||||||
username: dco_decode_String(arr[1]),
|
username: dco_decode_String(arr[1]),
|
||||||
|
|
@ -6003,40 +6003,41 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
storeMediaFilesInGallery: dco_decode_bool(arr[27]),
|
storeMediaFilesInGallery: dco_decode_bool(arr[27]),
|
||||||
autoStoreAllSendUnlimitedMediaFiles: dco_decode_bool(arr[28]),
|
autoStoreAllSendUnlimitedMediaFiles: dco_decode_bool(arr[28]),
|
||||||
typingIndicators: dco_decode_bool(arr[29]),
|
typingIndicators: dco_decode_bool(arr[29]),
|
||||||
showRestoreFlame: dco_decode_bool(arr[30]),
|
sealedSenderEnabled: dco_decode_bool(arr[30]),
|
||||||
myBestFriendGroupId: dco_decode_opt_String(arr[31]),
|
showRestoreFlame: dco_decode_bool(arr[31]),
|
||||||
|
myBestFriendGroupId: dco_decode_opt_String(arr[32]),
|
||||||
signalLastSignedPreKeyUpdated: dco_decode_opt_box_autoadd_Chrono_Utc(
|
signalLastSignedPreKeyUpdated: dco_decode_opt_box_autoadd_Chrono_Utc(
|
||||||
arr[32],
|
|
||||||
),
|
|
||||||
signalLastPqcPreKeysUploaded: dco_decode_opt_box_autoadd_Chrono_Utc(
|
|
||||||
arr[33],
|
arr[33],
|
||||||
),
|
),
|
||||||
allowErrorTrackingViaSentry: dco_decode_bool(arr[34]),
|
signalLastPqcPreKeysUploaded: dco_decode_opt_box_autoadd_Chrono_Utc(
|
||||||
screenLockEnabled: dco_decode_bool(arr[35]),
|
arr[34],
|
||||||
isCloudBackupEnabled: dco_decode_bool(arr[36]),
|
),
|
||||||
isUserDiscoveryEnabled: dco_decode_bool(arr[37]),
|
allowErrorTrackingViaSentry: dco_decode_bool(arr[35]),
|
||||||
requiredSendImages: dco_decode_i_64(arr[38]),
|
screenLockEnabled: dco_decode_bool(arr[36]),
|
||||||
userDiscoveryThreshold: dco_decode_u_8(arr[39]),
|
isCloudBackupEnabled: dco_decode_bool(arr[37]),
|
||||||
userDiscoveryRequiresManualApproval: dco_decode_bool(arr[40]),
|
isUserDiscoveryEnabled: dco_decode_bool(arr[38]),
|
||||||
userDiscoverySharePromotion: dco_decode_bool(arr[41]),
|
requiredSendImages: dco_decode_i_64(arr[39]),
|
||||||
userDiscoveryInitializationError: dco_decode_bool(arr[42]),
|
userDiscoveryThreshold: dco_decode_u_8(arr[40]),
|
||||||
askForFriendPromotions: dco_decode_bool(arr[43]),
|
userDiscoveryRequiresManualApproval: dco_decode_bool(arr[41]),
|
||||||
currentPreKeyIndexStart: dco_decode_i_64(arr[44]),
|
userDiscoverySharePromotion: dco_decode_bool(arr[42]),
|
||||||
currentSignedPreKeyIndexStart: dco_decode_i_64(arr[45]),
|
userDiscoveryInitializationError: dco_decode_bool(arr[43]),
|
||||||
lastChangeLogHash: dco_decode_opt_list_prim_u_8_strict(arr[46]),
|
askForFriendPromotions: dco_decode_bool(arr[44]),
|
||||||
hideChangeLog: dco_decode_bool(arr[47]),
|
currentPreKeyIndexStart: dco_decode_i_64(arr[45]),
|
||||||
hideMemoriesBackupPromo: dco_decode_bool(arr[48]),
|
currentSignedPreKeyIndexStart: dco_decode_i_64(arr[46]),
|
||||||
updateFcmToken: dco_decode_bool(arr[49]),
|
lastChangeLogHash: dco_decode_opt_list_prim_u_8_strict(arr[47]),
|
||||||
canUseLoginTokenForAuth: dco_decode_bool(arr[50]),
|
hideChangeLog: dco_decode_bool(arr[48]),
|
||||||
twonlySafeBackup: dco_decode_opt_box_autoadd_twonly_safe_backup(arr[51]),
|
hideMemoriesBackupPromo: dco_decode_bool(arr[49]),
|
||||||
isBackupEnabled: dco_decode_bool(arr[52]),
|
updateFcmToken: dco_decode_bool(arr[50]),
|
||||||
|
canUseLoginTokenForAuth: dco_decode_bool(arr[51]),
|
||||||
|
twonlySafeBackup: dco_decode_opt_box_autoadd_twonly_safe_backup(arr[52]),
|
||||||
|
isBackupEnabled: dco_decode_bool(arr[53]),
|
||||||
passwordLessRecovery:
|
passwordLessRecovery:
|
||||||
dco_decode_opt_box_autoadd_passwordless_recovery_config(arr[53]),
|
dco_decode_opt_box_autoadd_passwordless_recovery_config(arr[54]),
|
||||||
fcmToken: dco_decode_opt_String(arr[54]),
|
fcmToken: dco_decode_opt_String(arr[55]),
|
||||||
lastFcmWakeupAt: dco_decode_opt_box_autoadd_i_64(arr[55]),
|
lastFcmWakeupAt: dco_decode_opt_box_autoadd_i_64(arr[56]),
|
||||||
currentSetupPage: dco_decode_opt_String(arr[56]),
|
currentSetupPage: dco_decode_opt_String(arr[57]),
|
||||||
skipSetupPages: dco_decode_bool(arr[57]),
|
skipSetupPages: dco_decode_bool(arr[58]),
|
||||||
hasZoomed: dco_decode_bool(arr[58]),
|
hasZoomed: dco_decode_bool(arr[59]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -7103,6 +7104,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
var var_storeMediaFilesInGallery = sse_decode_bool(deserializer);
|
var var_storeMediaFilesInGallery = sse_decode_bool(deserializer);
|
||||||
var var_autoStoreAllSendUnlimitedMediaFiles = sse_decode_bool(deserializer);
|
var var_autoStoreAllSendUnlimitedMediaFiles = sse_decode_bool(deserializer);
|
||||||
var var_typingIndicators = sse_decode_bool(deserializer);
|
var var_typingIndicators = sse_decode_bool(deserializer);
|
||||||
|
var var_sealedSenderEnabled = sse_decode_bool(deserializer);
|
||||||
var var_showRestoreFlame = sse_decode_bool(deserializer);
|
var var_showRestoreFlame = sse_decode_bool(deserializer);
|
||||||
var var_myBestFriendGroupId = sse_decode_opt_String(deserializer);
|
var var_myBestFriendGroupId = sse_decode_opt_String(deserializer);
|
||||||
var var_signalLastSignedPreKeyUpdated =
|
var var_signalLastSignedPreKeyUpdated =
|
||||||
|
|
@ -7172,6 +7174,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
autoStoreAllSendUnlimitedMediaFiles:
|
autoStoreAllSendUnlimitedMediaFiles:
|
||||||
var_autoStoreAllSendUnlimitedMediaFiles,
|
var_autoStoreAllSendUnlimitedMediaFiles,
|
||||||
typingIndicators: var_typingIndicators,
|
typingIndicators: var_typingIndicators,
|
||||||
|
sealedSenderEnabled: var_sealedSenderEnabled,
|
||||||
showRestoreFlame: var_showRestoreFlame,
|
showRestoreFlame: var_showRestoreFlame,
|
||||||
myBestFriendGroupId: var_myBestFriendGroupId,
|
myBestFriendGroupId: var_myBestFriendGroupId,
|
||||||
signalLastSignedPreKeyUpdated: var_signalLastSignedPreKeyUpdated,
|
signalLastSignedPreKeyUpdated: var_signalLastSignedPreKeyUpdated,
|
||||||
|
|
@ -8291,6 +8294,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
sse_encode_bool(self.storeMediaFilesInGallery, serializer);
|
sse_encode_bool(self.storeMediaFilesInGallery, serializer);
|
||||||
sse_encode_bool(self.autoStoreAllSendUnlimitedMediaFiles, serializer);
|
sse_encode_bool(self.autoStoreAllSendUnlimitedMediaFiles, serializer);
|
||||||
sse_encode_bool(self.typingIndicators, serializer);
|
sse_encode_bool(self.typingIndicators, serializer);
|
||||||
|
sse_encode_bool(self.sealedSenderEnabled, serializer);
|
||||||
sse_encode_bool(self.showRestoreFlame, serializer);
|
sse_encode_bool(self.showRestoreFlame, serializer);
|
||||||
sse_encode_opt_String(self.myBestFriendGroupId, serializer);
|
sse_encode_opt_String(self.myBestFriendGroupId, serializer);
|
||||||
sse_encode_opt_box_autoadd_Chrono_Utc(
|
sse_encode_opt_box_autoadd_Chrono_Utc(
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,11 @@ class UserConfig {
|
||||||
bool storeMediaFilesInGallery;
|
bool storeMediaFilesInGallery;
|
||||||
bool autoStoreAllSendUnlimitedMediaFiles;
|
bool autoStoreAllSendUnlimitedMediaFiles;
|
||||||
bool typingIndicators;
|
bool typingIndicators;
|
||||||
|
|
||||||
|
/// Announces to contacts that this account accepts sealed-sender envelopes,
|
||||||
|
/// and lets this account send them. Both sides have to have it on before
|
||||||
|
/// anything travels sealed.
|
||||||
|
bool sealedSenderEnabled;
|
||||||
bool showRestoreFlame;
|
bool showRestoreFlame;
|
||||||
String? myBestFriendGroupId;
|
String? myBestFriendGroupId;
|
||||||
DateTime? signalLastSignedPreKeyUpdated;
|
DateTime? signalLastSignedPreKeyUpdated;
|
||||||
|
|
@ -198,6 +203,7 @@ class UserConfig {
|
||||||
required this.storeMediaFilesInGallery,
|
required this.storeMediaFilesInGallery,
|
||||||
required this.autoStoreAllSendUnlimitedMediaFiles,
|
required this.autoStoreAllSendUnlimitedMediaFiles,
|
||||||
required this.typingIndicators,
|
required this.typingIndicators,
|
||||||
|
required this.sealedSenderEnabled,
|
||||||
required this.showRestoreFlame,
|
required this.showRestoreFlame,
|
||||||
this.myBestFriendGroupId,
|
this.myBestFriendGroupId,
|
||||||
this.signalLastSignedPreKeyUpdated,
|
this.signalLastSignedPreKeyUpdated,
|
||||||
|
|
@ -261,6 +267,7 @@ class UserConfig {
|
||||||
storeMediaFilesInGallery.hashCode ^
|
storeMediaFilesInGallery.hashCode ^
|
||||||
autoStoreAllSendUnlimitedMediaFiles.hashCode ^
|
autoStoreAllSendUnlimitedMediaFiles.hashCode ^
|
||||||
typingIndicators.hashCode ^
|
typingIndicators.hashCode ^
|
||||||
|
sealedSenderEnabled.hashCode ^
|
||||||
showRestoreFlame.hashCode ^
|
showRestoreFlame.hashCode ^
|
||||||
myBestFriendGroupId.hashCode ^
|
myBestFriendGroupId.hashCode ^
|
||||||
signalLastSignedPreKeyUpdated.hashCode ^
|
signalLastSignedPreKeyUpdated.hashCode ^
|
||||||
|
|
@ -329,6 +336,7 @@ class UserConfig {
|
||||||
autoStoreAllSendUnlimitedMediaFiles ==
|
autoStoreAllSendUnlimitedMediaFiles ==
|
||||||
other.autoStoreAllSendUnlimitedMediaFiles &&
|
other.autoStoreAllSendUnlimitedMediaFiles &&
|
||||||
typingIndicators == other.typingIndicators &&
|
typingIndicators == other.typingIndicators &&
|
||||||
|
sealedSenderEnabled == other.sealedSenderEnabled &&
|
||||||
showRestoreFlame == other.showRestoreFlame &&
|
showRestoreFlame == other.showRestoreFlame &&
|
||||||
myBestFriendGroupId == other.myBestFriendGroupId &&
|
myBestFriendGroupId == other.myBestFriendGroupId &&
|
||||||
signalLastSignedPreKeyUpdated ==
|
signalLastSignedPreKeyUpdated ==
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,6 @@ class AppState {
|
||||||
static bool isInBackgroundTask = false;
|
static bool isInBackgroundTask = false;
|
||||||
static bool allowErrorTrackingViaSentry = false;
|
static bool allowErrorTrackingViaSentry = false;
|
||||||
static bool gotMessageFromServer = false;
|
static bool gotMessageFromServer = false;
|
||||||
static int latestAppVersionId = 118;
|
static int latestAppVersionId = 119;
|
||||||
static bool hasCameraPermissions = false;
|
static bool hasCameraPermissions = false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,10 @@ enum MessageActionType {
|
||||||
openedAt,
|
openedAt,
|
||||||
ackByUserAt,
|
ackByUserAt,
|
||||||
ackByServerAt,
|
ackByServerAt,
|
||||||
|
|
||||||
|
/// Present when this recipient's copy left as a sealed-sender envelope. The
|
||||||
|
/// transport is per recipient, so in a group only some members may have one.
|
||||||
|
sealedSenderAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
@DataClassName('MessageAction')
|
@DataClassName('MessageAction')
|
||||||
|
|
|
||||||
|
|
@ -1832,6 +1832,30 @@ abstract class AppLocalizations {
|
||||||
/// **'Waiting for internet'**
|
/// **'Waiting for internet'**
|
||||||
String get waitingForInternet;
|
String get waitingForInternet;
|
||||||
|
|
||||||
|
/// No description provided for @sealedSenderTransportSealed.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Sealed sender'**
|
||||||
|
String get sealedSenderTransportSealed;
|
||||||
|
|
||||||
|
/// No description provided for @sealedSenderTransportStandard.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Standard'**
|
||||||
|
String get sealedSenderTransportStandard;
|
||||||
|
|
||||||
|
/// No description provided for @settingsSealedSender.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Sealed Sender'**
|
||||||
|
String get settingsSealedSender;
|
||||||
|
|
||||||
|
/// No description provided for @settingsSealedSenderSubtitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Hides who you are from the server when sending. Both you and your contact need it turned on, so turning it off also stops your contacts from sending to you sealed.'**
|
||||||
|
String get settingsSealedSenderSubtitle;
|
||||||
|
|
||||||
/// No description provided for @editHistory.
|
/// No description provided for @editHistory.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
|
||||||
|
|
@ -980,6 +980,19 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get waitingForInternet => 'Warten auf Internet';
|
String get waitingForInternet => 'Warten auf Internet';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sealedSenderTransportSealed => 'Sealed Sender';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sealedSenderTransportStandard => 'Standard';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settingsSealedSender => 'Sealed Sender';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settingsSealedSenderSubtitle =>
|
||||||
|
'Verbirgt beim Senden vor dem Server, wer du bist. Sowohl du als auch dein Kontakt müssen es aktiviert haben; deaktivierst du es, senden deine Kontakte auch nicht mehr versiegelt an dich.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get editHistory => 'Bearbeitungshistorie';
|
String get editHistory => 'Bearbeitungshistorie';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -973,6 +973,19 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get waitingForInternet => 'Waiting for internet';
|
String get waitingForInternet => 'Waiting for internet';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sealedSenderTransportSealed => 'Sealed sender';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sealedSenderTransportStandard => 'Standard';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settingsSealedSender => 'Sealed Sender';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settingsSealedSenderSubtitle =>
|
||||||
|
'Hides who you are from the server when sending. Both you and your contact need it turned on, so turning it off also stops your contacts from sending to you sealed.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get editHistory => 'Edit history';
|
String get editHistory => 'Edit history';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -108,9 +108,17 @@ Future<void> runMigrations() async {
|
||||||
await UserService.update((u) => u.appVersion = 118);
|
await UserService.update((u) => u.appVersion = 118);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The server only delivers sealed-sender payloads to clients reporting at
|
||||||
|
// least this version, so the bump is what announces support for them. The
|
||||||
|
// `sealedSenderEnabled` setting itself needs no migration: an existing
|
||||||
|
// user.json without the key falls back to the enabled default.
|
||||||
|
if (userService.currentUser.appVersion < 119) {
|
||||||
|
await UserService.update((u) => u.appVersion = 119);
|
||||||
|
}
|
||||||
|
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
assert(
|
assert(
|
||||||
AppState.latestAppVersionId == 118,
|
AppState.latestAppVersionId == 119,
|
||||||
'Forgot to update the target version in runMigrations() after incrementing AppState.latestAppVersionId.',
|
'Forgot to update the target version in runMigrations() after incrementing AppState.latestAppVersionId.',
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,11 @@ class _MessageInfoViewState extends State<MessageInfoView> {
|
||||||
t.$1.type == MessageActionType.openedAt &&
|
t.$1.type == MessageActionType.openedAt &&
|
||||||
t.$2.userId == groupMember.$2.userId,
|
t.$2.userId == groupMember.$2.userId,
|
||||||
);
|
);
|
||||||
|
final sealedSender = messageActions.firstWhereOrNull(
|
||||||
|
(t) =>
|
||||||
|
t.$1.type == MessageActionType.sealedSenderAt &&
|
||||||
|
t.$2.userId == groupMember.$2.userId,
|
||||||
|
);
|
||||||
|
|
||||||
var actionTypeText = context.lang.waitingForInternet;
|
var actionTypeText = context.lang.waitingForInternet;
|
||||||
var actionAt = widget.message.createdAt;
|
var actionAt = widget.message.createdAt;
|
||||||
|
|
@ -158,6 +163,18 @@ class _MessageInfoViewState extends State<MessageInfoView> {
|
||||||
style: const TextStyle(fontSize: 12),
|
style: const TextStyle(fontSize: 12),
|
||||||
),
|
),
|
||||||
Text(actionTypeText),
|
Text(actionTypeText),
|
||||||
|
// The transport is decided per recipient, so it is only known
|
||||||
|
// once this member's copy has actually left the device.
|
||||||
|
if (ackByServer != null)
|
||||||
|
Text(
|
||||||
|
sealedSender != null
|
||||||
|
? context.lang.sealedSenderTransportSealed
|
||||||
|
: context.lang.sealedSenderTransportStandard,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,13 @@ class _PrivacyViewState extends State<PrivacyView> {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> toggleSealedSender() async {
|
||||||
|
await UserService.update((u) {
|
||||||
|
u.sealedSenderEnabled = !u.sealedSenderEnabled;
|
||||||
|
});
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|
@ -93,6 +100,16 @@ class _PrivacyViewState extends State<PrivacyView> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
|
ListTile(
|
||||||
|
title: Text(context.lang.settingsSealedSender),
|
||||||
|
subtitle: Text(context.lang.settingsSealedSenderSubtitle),
|
||||||
|
onTap: toggleSealedSender,
|
||||||
|
trailing: Switch.adaptive(
|
||||||
|
value: userService.currentUser.sealedSenderEnabled,
|
||||||
|
onChanged: (a) => toggleSealedSender(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(context.lang.settingsScreenLock),
|
title: Text(context.lang.settingsScreenLock),
|
||||||
subtitle: Text(context.lang.settingsScreenLockSubtitle),
|
subtitle: Text(context.lang.settingsScreenLockSubtitle),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT EXISTS(SELECT 1 FROM contacts WHERE user_id = ? AND sealed_sender_enabled = 1)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "EXISTS(SELECT 1 FROM contacts WHERE user_id = ? AND sealed_sender_enabled = 1)",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Integer",
|
||||||
|
"origin": "Expression"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "0e2dcd586093544fb7c0163785648b2e2510cc8ca43f9f418e60bfc5afb40d48"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "DELETE FROM message_actions\n WHERE message_id = ? AND contact_id = ? AND type = 'sealedSenderAt'",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "1c0946be221d955a0082ba67f7041bb914b6f3a319967765240c1ac958768006"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "INSERT INTO privacy_pass_tokens(token, expires_at) VALUES (?, ?)\n ON CONFLICT(token) DO NOTHING",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "30e32daf98cb4d71d84b79f214d828524a4450590d72e2310eb89bb7033f4399"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT COUNT(*) FROM privacy_pass_tokens WHERE expires_at > ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "COUNT(*)",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Integer",
|
||||||
|
"origin": "Expression"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "3ef50101727d2255e0ae23e4151336652a52d58403ce2e2552b53de6cb8b8743"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT sealed_sender_enabled FROM contacts WHERE user_id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "sealed_sender_enabled",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Integer",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "contacts",
|
||||||
|
"name": "sealed_sender_enabled"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "407232f4e5dae0ed496d80369d0bb5f158b7fadb9b74db4fb922e26af0443a63"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n INSERT INTO message_actions(message_id, contact_id, type)\n VALUES (?, ?, 'sealedSenderAt')\n ON CONFLICT(message_id, contact_id, type)\n DO UPDATE SET action_at = CAST(strftime('%s', 'now') AS INTEGER)\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "738756d95a2482031a39318684279522c5628f5b5e61a45904283734e14d645a"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "UPDATE contacts SET sealed_sender_enabled = ?\n WHERE user_id = ? AND sealed_sender_enabled != ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 3
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "74c9a8f699b6a1c214e196824b9ac482d6f7165af2fa5a2e275facb310acdaee"
|
||||||
|
}
|
||||||
|
|
@ -288,6 +288,17 @@
|
||||||
"name": "media_received_counter"
|
"name": "media_received_counter"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sealed_sender_enabled",
|
||||||
|
"ordinal": 26,
|
||||||
|
"type_info": "Integer",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "contacts",
|
||||||
|
"name": "sealed_sender_enabled"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
|
|
@ -319,6 +330,7 @@
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT EXISTS(SELECT 1 FROM message_actions\n WHERE message_id = ? AND contact_id = ? AND type = 'sealedSenderAt')",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "EXISTS(SELECT 1 FROM message_actions\n WHERE message_id = ? AND contact_id = ? AND type = 'sealedSenderAt')",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Integer",
|
||||||
|
"origin": "Expression"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "c91dc6e6cff692209fa18216e2b1ec547556fe464ceb3993d595f8f2c7d8532f"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT COUNT(*) FROM privacy_pass_tokens",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "COUNT(*)",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Integer",
|
||||||
|
"origin": "Expression"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 0
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "d84d22b95288a8fd4bbcb96bdddbfd69afb18384d91764009abb50610c959374"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "DELETE FROM privacy_pass_tokens WHERE expires_at <= ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "e33bd4d14da2ab7a0b083d894f911a582bd664e168ed16e0ff6adb7532bddaec"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "DELETE FROM privacy_pass_tokens\n WHERE token = (\n SELECT token FROM privacy_pass_tokens\n WHERE expires_at > ?\n ORDER BY expires_at ASC\n LIMIT 1\n )\n RETURNING token",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "token",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Blob",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "privacy_pass_tokens",
|
||||||
|
"name": "token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "f4af415f434d17c2880be292539d72ae5c0b8d1176a6c9860716832c1d82885b"
|
||||||
|
}
|
||||||
455
rust/Cargo.lock
generated
455
rust/Cargo.lock
generated
|
|
@ -24,7 +24,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
|
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crypto-common 0.1.7",
|
"crypto-common 0.1.7",
|
||||||
"generic-array",
|
"generic-array 0.14.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -256,12 +256,30 @@ dependencies = [
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base16ct"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base16ct"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.22.1"
|
version = "0.22.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64ct"
|
||||||
|
version = "1.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bindgen"
|
name = "bindgen"
|
||||||
version = "0.72.1"
|
version = "0.72.1"
|
||||||
|
|
@ -308,13 +326,33 @@ dependencies = [
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "blind-rsa-signatures"
|
||||||
|
version = "0.17.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f7c8e1ec3966bafbe115ad484420b260f5fabf88528e4ef8cd3024ffedb50e46"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-bigint 0.7.5",
|
||||||
|
"crypto-primes",
|
||||||
|
"ct-codecs",
|
||||||
|
"derive-new",
|
||||||
|
"derive_more",
|
||||||
|
"digest 0.11.3",
|
||||||
|
"hmac-sha256",
|
||||||
|
"hmac-sha512",
|
||||||
|
"rand 0.10.2",
|
||||||
|
"rand_core 0.10.1",
|
||||||
|
"rsa",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.10.4"
|
version = "0.10.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"generic-array",
|
"generic-array 0.14.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -420,7 +458,7 @@ version = "0.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nom",
|
"nom 7.1.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -553,6 +591,12 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const-oid"
|
||||||
|
version = "0.9.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "const-oid"
|
name = "const-oid"
|
||||||
version = "0.10.2"
|
version = "0.10.2"
|
||||||
|
|
@ -565,6 +609,15 @@ version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "18f12cc9948ed9604230cdddc7c86e270f9401ccbe3c2e98a4378c5e7632212f"
|
checksum = "18f12cc9948ed9604230cdddc7c86e270f9401ccbe3c2e98a4378c5e7632212f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "convert_case"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-segmentation",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
|
|
@ -709,13 +762,39 @@ version = "0.8.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
|
checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-bigint"
|
||||||
|
version = "0.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array 0.14.7",
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-bigint"
|
||||||
|
version = "0.7.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271"
|
||||||
|
dependencies = [
|
||||||
|
"cpubits",
|
||||||
|
"ctutils",
|
||||||
|
"num-traits",
|
||||||
|
"rand_core 0.10.1",
|
||||||
|
"serdect",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-common"
|
name = "crypto-common"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"generic-array",
|
"generic-array 0.14.7",
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
@ -730,6 +809,23 @@ dependencies = [
|
||||||
"rand_core 0.10.1",
|
"rand_core 0.10.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-primes"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "21f41f23de7d24cdbda7f0c4d9c0351f99a4ceb258ef30e5c1927af8987ffe5a"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-bigint 0.7.5",
|
||||||
|
"libm",
|
||||||
|
"rand_core 0.10.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ct-codecs"
|
||||||
|
version = "1.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "49fb0c6640b4507ebd99ff67677009e381ba5eee1d14df78de4a3d16eb123c39"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ctr"
|
name = "ctr"
|
||||||
version = "0.9.2"
|
version = "0.9.2"
|
||||||
|
|
@ -757,6 +853,23 @@ dependencies = [
|
||||||
"cmov",
|
"cmov",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "curve25519-dalek"
|
||||||
|
version = "4.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures 0.2.17",
|
||||||
|
"curve25519-dalek-derive",
|
||||||
|
"fiat-crypto 0.2.9",
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
"rustc_version",
|
||||||
|
"serde",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "curve25519-dalek"
|
name = "curve25519-dalek"
|
||||||
version = "5.0.0"
|
version = "5.0.0"
|
||||||
|
|
@ -767,7 +880,7 @@ dependencies = [
|
||||||
"cpufeatures 0.3.0",
|
"cpufeatures 0.3.0",
|
||||||
"curve25519-dalek-derive",
|
"curve25519-dalek-derive",
|
||||||
"digest 0.11.3",
|
"digest 0.11.3",
|
||||||
"fiat-crypto",
|
"fiat-crypto 0.3.0",
|
||||||
"rand_core 0.10.1",
|
"rand_core 0.10.1",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
"subtle",
|
"subtle",
|
||||||
|
|
@ -884,12 +997,44 @@ dependencies = [
|
||||||
"syn 2.0.119",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "der"
|
||||||
|
version = "0.7.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
||||||
|
dependencies = [
|
||||||
|
"const-oid 0.9.6",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "der"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d"
|
||||||
|
dependencies = [
|
||||||
|
"const-oid 0.10.2",
|
||||||
|
"pem-rfc7468",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.5.8"
|
version = "0.5.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive-new"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2cdc8d50f426189eef89dac62fabfa0abb27d5cc008f25bf4156a0203325becc"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive-where"
|
name = "derive-where"
|
||||||
version = "1.6.1"
|
version = "1.6.1"
|
||||||
|
|
@ -927,10 +1072,12 @@ version = "2.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
|
checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"convert_case",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
"syn 2.0.119",
|
"syn 2.0.119",
|
||||||
|
"unicode-xid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -951,7 +1098,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
|
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer 0.12.1",
|
"block-buffer 0.12.1",
|
||||||
"const-oid",
|
"const-oid 0.10.2",
|
||||||
"crypto-common 0.2.2",
|
"crypto-common 0.2.2",
|
||||||
"ctutils",
|
"ctutils",
|
||||||
]
|
]
|
||||||
|
|
@ -988,6 +1135,24 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "elliptic-curve"
|
||||||
|
version = "0.13.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
|
||||||
|
dependencies = [
|
||||||
|
"base16ct 0.2.0",
|
||||||
|
"crypto-bigint 0.5.5",
|
||||||
|
"digest 0.10.7",
|
||||||
|
"ff",
|
||||||
|
"generic-array 0.14.7",
|
||||||
|
"group",
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
"sec1",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "embedded-io"
|
name = "embedded-io"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
|
@ -1094,6 +1259,22 @@ dependencies = [
|
||||||
"simd-adler32",
|
"simd-adler32",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ff"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
|
||||||
|
dependencies = [
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fiat-crypto"
|
||||||
|
version = "0.2.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fiat-crypto"
|
name = "fiat-crypto"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
|
@ -1337,6 +1518,18 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"typenum",
|
"typenum",
|
||||||
"version_check",
|
"version_check",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "generic-array"
|
||||||
|
version = "1.4.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "337d46834ee672ab3e48caca2cb0c78cc174fb12b3a68d0d88f99a0519a5e36e"
|
||||||
|
dependencies = [
|
||||||
|
"rustversion",
|
||||||
|
"serde_core",
|
||||||
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1410,6 +1603,17 @@ version = "0.3.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
|
checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "group"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
|
||||||
|
dependencies = [
|
||||||
|
"ff",
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hash32"
|
name = "hash32"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
|
@ -1567,6 +1771,24 @@ dependencies = [
|
||||||
"digest 0.11.3",
|
"digest 0.11.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hmac-sha256"
|
||||||
|
version = "1.1.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec9d92d097f4749b64e8cc33d924d9f40a2d4eb91402b458014b781f5733d60f"
|
||||||
|
dependencies = [
|
||||||
|
"digest 0.11.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hmac-sha512"
|
||||||
|
version = "1.1.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "019ece39bbefc17f13f677a690328cb978dbf6790e141a3c24e66372cb38588b"
|
||||||
|
dependencies = [
|
||||||
|
"digest 0.11.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hpke-rs"
|
name = "hpke-rs"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
|
|
@ -1865,7 +2087,7 @@ version = "0.1.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"generic-array",
|
"generic-array 0.14.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2143,7 +2365,7 @@ name = "libsignal-core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/signalapp/libsignal?rev=44a6dd8fc9f9d6903b32842b80b3894a75678418#44a6dd8fc9f9d6903b32842b80b3894a75678418"
|
source = "git+https://github.com/signalapp/libsignal?rev=44a6dd8fc9f9d6903b32842b80b3894a75678418#44a6dd8fc9f9d6903b32842b80b3894a75678418"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"curve25519-dalek",
|
"curve25519-dalek 5.0.0",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"libsignal-debug",
|
"libsignal-debug",
|
||||||
|
|
@ -2350,6 +2572,15 @@ dependencies = [
|
||||||
"minimal-lexical",
|
"minimal-lexical",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nom"
|
||||||
|
version = "8.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nu-ansi-term"
|
name = "nu-ansi-term"
|
||||||
version = "0.50.3"
|
version = "0.50.3"
|
||||||
|
|
@ -2510,6 +2741,17 @@ dependencies = [
|
||||||
"log",
|
"log",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "p384"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6"
|
||||||
|
dependencies = [
|
||||||
|
"elliptic-curve",
|
||||||
|
"primeorder",
|
||||||
|
"sha2 0.10.9",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking"
|
name = "parking"
|
||||||
version = "2.2.1"
|
version = "2.2.1"
|
||||||
|
|
@ -2561,6 +2803,15 @@ dependencies = [
|
||||||
"hmac 0.13.0",
|
"hmac 0.13.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pem-rfc7468"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.2"
|
version = "2.3.2"
|
||||||
|
|
@ -2590,6 +2841,26 @@ version = "0.2.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkcs1"
|
||||||
|
version = "0.8.0-rc.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "986d2e952779af96ea048f160fd9194e1751b4faea78bcf3ceb456efe008088e"
|
||||||
|
dependencies = [
|
||||||
|
"der 0.8.1",
|
||||||
|
"spki",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkcs8"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7"
|
||||||
|
dependencies = [
|
||||||
|
"der 0.8.1",
|
||||||
|
"spki",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkg-config"
|
name = "pkg-config"
|
||||||
version = "0.3.33"
|
version = "0.3.33"
|
||||||
|
|
@ -2717,6 +2988,40 @@ dependencies = [
|
||||||
"syn 3.0.3",
|
"syn 3.0.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "primeorder"
|
||||||
|
version = "0.13.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
|
||||||
|
dependencies = [
|
||||||
|
"elliptic-curve",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "privacypass"
|
||||||
|
version = "0.2.0-pre.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c5da5a5d50ec311674442d7388689094d044d47c783a2f3dd5cf4ef572be92a2"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"base64",
|
||||||
|
"blind-rsa-signatures",
|
||||||
|
"generic-array 1.4.5",
|
||||||
|
"http",
|
||||||
|
"log",
|
||||||
|
"nom 8.0.0",
|
||||||
|
"p384",
|
||||||
|
"rand 0.10.2",
|
||||||
|
"serde",
|
||||||
|
"sha2 0.10.9",
|
||||||
|
"subtle",
|
||||||
|
"thiserror 2.0.19",
|
||||||
|
"tls_codec",
|
||||||
|
"trait-variant",
|
||||||
|
"typenum",
|
||||||
|
"voprf-ng",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-crate"
|
name = "proc-macro-crate"
|
||||||
version = "3.5.0"
|
version = "3.5.0"
|
||||||
|
|
@ -3109,6 +3414,26 @@ version = "0.20.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
|
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rsa"
|
||||||
|
version = "0.10.0-rc.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "30b2aa4ba0d89f73d1e332df05be0eeab8840351c36ca5654341dfdb57bb3caf"
|
||||||
|
dependencies = [
|
||||||
|
"const-oid 0.10.2",
|
||||||
|
"crypto-bigint 0.7.5",
|
||||||
|
"crypto-primes",
|
||||||
|
"digest 0.11.3",
|
||||||
|
"pkcs1",
|
||||||
|
"pkcs8",
|
||||||
|
"rand_core 0.10.1",
|
||||||
|
"serde",
|
||||||
|
"serdect",
|
||||||
|
"signature",
|
||||||
|
"spki",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust_lib_twonly"
|
name = "rust_lib_twonly"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -3133,9 +3458,11 @@ dependencies = [
|
||||||
"keyring-core",
|
"keyring-core",
|
||||||
"libsignal-protocol",
|
"libsignal-protocol",
|
||||||
"libsqlite3-sys",
|
"libsqlite3-sys",
|
||||||
|
"p384",
|
||||||
"paste",
|
"paste",
|
||||||
"postcard",
|
"postcard",
|
||||||
"pretty_env_logger",
|
"pretty_env_logger",
|
||||||
|
"privacypass",
|
||||||
"prost",
|
"prost",
|
||||||
"prost-build",
|
"prost-build",
|
||||||
"rand 0.8.7",
|
"rand 0.8.7",
|
||||||
|
|
@ -3320,6 +3647,19 @@ dependencies = [
|
||||||
"sha2 0.11.0",
|
"sha2 0.11.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sec1"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
|
||||||
|
dependencies = [
|
||||||
|
"base16ct 0.2.0",
|
||||||
|
"der 0.7.10",
|
||||||
|
"generic-array 0.14.7",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "security-framework"
|
name = "security-framework"
|
||||||
version = "3.7.0"
|
version = "3.7.0"
|
||||||
|
|
@ -3404,6 +3744,16 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serdect"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e"
|
||||||
|
dependencies = [
|
||||||
|
"base16ct 1.0.0",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha1"
|
name = "sha1"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
|
|
@ -3505,6 +3855,16 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signature"
|
||||||
|
version = "3.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5"
|
||||||
|
dependencies = [
|
||||||
|
"digest 0.11.3",
|
||||||
|
"rand_core 0.10.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "simd-adler32"
|
name = "simd-adler32"
|
||||||
version = "0.3.10"
|
version = "0.3.10"
|
||||||
|
|
@ -3575,13 +3935,23 @@ dependencies = [
|
||||||
"lock_api",
|
"lock_api",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "spki"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"der 0.8.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spqr"
|
name = "spqr"
|
||||||
version = "1.5.3"
|
version = "1.5.3"
|
||||||
source = "git+https://github.com/signalapp/SparsePostQuantumRatchet.git?tag=v1.5.3#fd320484dcec89004021e6fdc7481825f5f261fa"
|
source = "git+https://github.com/signalapp/SparsePostQuantumRatchet.git?tag=v1.5.3#fd320484dcec89004021e6fdc7481825f5f261fa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cpufeatures 0.3.0",
|
"cpufeatures 0.3.0",
|
||||||
"curve25519-dalek",
|
"curve25519-dalek 5.0.0",
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"hax-lib",
|
"hax-lib",
|
||||||
"hkdf 0.13.0",
|
"hkdf 0.13.0",
|
||||||
|
|
@ -3701,7 +4071,7 @@ dependencies = [
|
||||||
"either",
|
"either",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"generic-array",
|
"generic-array 0.14.7",
|
||||||
"log",
|
"log",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -4053,6 +4423,27 @@ version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tls_codec"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "18cc98286004cea38f717e2b03d990fc774fbfd38a82720de40e5c94365067c8"
|
||||||
|
dependencies = [
|
||||||
|
"tls_codec_derive",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tls_codec_derive"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "674f41dd95f76cdeb005c83f9444e20cf43daebef37cde9e49a9ca6f4e87b423"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.53.1"
|
version = "1.53.1"
|
||||||
|
|
@ -4294,6 +4685,17 @@ dependencies = [
|
||||||
"tracing-log",
|
"tracing-log",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "trait-variant"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b19a4867a870f6edc4c283f2b455804b1879c0baf0e642f26b03ed8ee262d9d3"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "try-lock"
|
name = "try-lock"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
|
|
@ -4380,12 +4782,24 @@ version = "0.5.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee"
|
checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-segmentation"
|
||||||
|
version = "1.13.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-vo"
|
name = "unicode-vo"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94"
|
checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-xid"
|
||||||
|
version = "0.2.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "universal-hash"
|
name = "universal-hash"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
|
|
@ -4492,6 +4906,25 @@ version = "0.9.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "voprf-ng"
|
||||||
|
version = "0.6.0-pre.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bed8944bdc5dfafa7b1c434c086b3f6281dfb26715bd8e42d58d5e4434cc0307"
|
||||||
|
dependencies = [
|
||||||
|
"curve25519-dalek 4.1.3",
|
||||||
|
"derive-where",
|
||||||
|
"digest 0.10.7",
|
||||||
|
"displaydoc",
|
||||||
|
"elliptic-curve",
|
||||||
|
"generic-array 1.4.5",
|
||||||
|
"rand_core 0.10.1",
|
||||||
|
"serde",
|
||||||
|
"sha2 0.10.9",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "walkdir"
|
name = "walkdir"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
|
|
@ -4859,7 +5292,7 @@ version = "3.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e7e8131a03190127fb2263afc72b322ecadae46b6ff8c6f399ff5d02f5559af6"
|
checksum = "e7e8131a03190127fb2263afc72b322ecadae46b6ff8c6f399ff5d02f5559af6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"curve25519-dalek",
|
"curve25519-dalek 5.0.0",
|
||||||
"rand_core 0.10.1",
|
"rand_core 0.10.1",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,11 @@ rustls = { version = "0.23.43", default-features = false, features = [
|
||||||
] }
|
] }
|
||||||
tokio-util = "0.7.16"
|
tokio-util = "0.7.16"
|
||||||
bon = "3.9.3"
|
bon = "3.9.3"
|
||||||
|
privacypass = "=0.2.0-pre.3"
|
||||||
|
p384 = { version = "0.13.0", default-features = false, features = [
|
||||||
|
"hash2curve",
|
||||||
|
"voprf",
|
||||||
|
] }
|
||||||
stream-tungstenite = "0.6.1"
|
stream-tungstenite = "0.6.1"
|
||||||
[target.'cfg(target_os = "ios")'.dependencies]
|
[target.'cfg(target_os = "ios")'.dependencies]
|
||||||
# iOS backend: Requires the 'protected' feature for Data Protection Keychain
|
# iOS backend: Requires the 'protected' feature for Data Protection Keychain
|
||||||
|
|
|
||||||
|
|
@ -461,6 +461,10 @@ async fn handle_encrypted_inner(
|
||||||
|
|
||||||
contact::check_for_profile_update(t, from_user_id, &content).await?;
|
contact::check_for_profile_update(t, from_user_id, &content).await?;
|
||||||
|
|
||||||
|
if let Some(enabled) = content.sender_accepts_sealed_sender {
|
||||||
|
Contact::set_sealed_sender_enabled(t, from_user_id, enabled).await?;
|
||||||
|
}
|
||||||
|
|
||||||
if content.ask_for_friend_promotions == Some(true) {
|
if content.ask_for_friend_promotions == Some(true) {
|
||||||
Contact::update_ask_for_friend_promotions(t, from_user_id).await?;
|
Contact::update_ask_for_friend_promotions(t, from_user_id).await?;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ use crate::context::Context;
|
||||||
use crate::database::app::tables::{Contact, MediaFile, NewReceipt, Receipt};
|
use crate::database::app::tables::{Contact, MediaFile, NewReceipt, Receipt};
|
||||||
use crate::error::{Result, TwonlyError};
|
use crate::error::{Result, TwonlyError};
|
||||||
use crate::services::contacts::ContactService;
|
use crate::services::contacts::ContactService;
|
||||||
|
use crate::services::sealed_sender::SealedSenderService;
|
||||||
use crate::utils::new_uuid_v4;
|
use crate::utils::new_uuid_v4;
|
||||||
use prost::Message as ProstMessage;
|
use prost::Message as ProstMessage;
|
||||||
use proto::encrypted_content::error_messages::Type;
|
use proto::encrypted_content::error_messages::Type;
|
||||||
|
|
@ -295,10 +296,13 @@ async fn encrypt_v2_with_session_recovery(
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct PreparedQueuedReceipt {
|
pub(crate) struct PreparedQueuedReceipt {
|
||||||
|
pub receipt_id: String,
|
||||||
pub contact_id: i64,
|
pub contact_id: i64,
|
||||||
pub message_id: Option<String>,
|
pub message_id: Option<String>,
|
||||||
pub contact_will_sends_receipt: i64,
|
pub contact_will_sends_receipt: i64,
|
||||||
pub payload: Vec<u8>,
|
/// The fully prepared client message. A sealed-sender envelope wraps this
|
||||||
|
/// value, while the named transport sends its encoding.
|
||||||
|
pub message: proto::Message,
|
||||||
pub wake_receiver: bool,
|
pub wake_receiver: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -391,10 +395,11 @@ async fn prepare_queued_receipt_from_row(
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(PreparedQueuedReceipt {
|
Ok(PreparedQueuedReceipt {
|
||||||
|
receipt_id: receipt_id.to_owned(),
|
||||||
contact_id: row.contact_id,
|
contact_id: row.contact_id,
|
||||||
message_id: row.message_id,
|
message_id: row.message_id,
|
||||||
contact_will_sends_receipt: row.contact_will_sends_receipt,
|
contact_will_sends_receipt: row.contact_will_sends_receipt,
|
||||||
payload: message.encode_to_vec(),
|
message,
|
||||||
wake_receiver: row.wake_receiver != 0,
|
wake_receiver: row.wake_receiver != 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -405,7 +410,7 @@ pub(crate) async fn prepare_queued_receipt(
|
||||||
) -> Result<Option<Vec<u8>>> {
|
) -> Result<Option<Vec<u8>>> {
|
||||||
Ok(prepare_queued_receipt_details(ctx, receipt_id)
|
Ok(prepare_queued_receipt_details(ctx, receipt_id)
|
||||||
.await?
|
.await?
|
||||||
.map(|receipt| receipt.payload))
|
.map(|receipt| receipt.message.encode_to_vec()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn send_queued_receipt(ctx: &Arc<Context>, receipt_id: &str) -> Result<()> {
|
pub(crate) async fn send_queued_receipt(ctx: &Arc<Context>, receipt_id: &str) -> Result<()> {
|
||||||
|
|
@ -440,20 +445,24 @@ pub(crate) async fn send_queued_receipt(ctx: &Arc<Context>, receipt_id: &str) ->
|
||||||
|
|
||||||
let receipt = prepare_queued_receipt_from_row(ctx, receipt_id, row).await?;
|
let receipt = prepare_queued_receipt_from_row(ctx, receipt_id, row).await?;
|
||||||
|
|
||||||
match Server::send_text_message(
|
let sent_sealed = SealedSenderService::try_send(ctx, &receipt).await?;
|
||||||
ctx,
|
|
||||||
receipt.contact_id,
|
if !sent_sealed {
|
||||||
receipt.payload,
|
match Server::send_text_message(
|
||||||
receipt.wake_receiver,
|
ctx,
|
||||||
)
|
receipt.contact_id,
|
||||||
.await?
|
receipt.message.encode_to_vec(),
|
||||||
{
|
receipt.wake_receiver,
|
||||||
ServerResult::Ok(()) => {}
|
)
|
||||||
ServerResult::ErrorCode(code) => {
|
.await?
|
||||||
return Err(TwonlyError::Generic(format!(
|
{
|
||||||
"server rejected sender delivery receipt with code: {}",
|
ServerResult::Ok(()) => {}
|
||||||
code
|
ServerResult::ErrorCode(code) => {
|
||||||
)));
|
return Err(TwonlyError::Generic(format!(
|
||||||
|
"server rejected sender delivery receipt with code: {}",
|
||||||
|
code
|
||||||
|
)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -472,6 +481,33 @@ pub(crate) async fn send_queued_receipt(ctx: &Arc<Context>, receipt_id: &str) ->
|
||||||
.execute(&mut *t)
|
.execute(&mut *t)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
// The transport is per recipient: in a group one member may accept
|
||||||
|
// sealed envelopes while another still needs the named send. Recording
|
||||||
|
// it as an action keeps that distinction visible per member.
|
||||||
|
if sent_sealed {
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO message_actions(message_id, contact_id, type)
|
||||||
|
VALUES (?, ?, 'sealedSenderAt')
|
||||||
|
ON CONFLICT(message_id, contact_id, type)
|
||||||
|
DO UPDATE SET action_at = CAST(strftime('%s', 'now') AS INTEGER)
|
||||||
|
"#,
|
||||||
|
message_id,
|
||||||
|
receipt.contact_id,
|
||||||
|
)
|
||||||
|
.execute(&mut *t)
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
sqlx::query!(
|
||||||
|
"DELETE FROM message_actions
|
||||||
|
WHERE message_id = ? AND contact_id = ? AND type = 'sealedSenderAt'",
|
||||||
|
message_id,
|
||||||
|
receipt.contact_id,
|
||||||
|
)
|
||||||
|
.execute(&mut *t)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
// `message_actions` keeps the per-recipient acknowledgement used by
|
// `message_actions` keeps the per-recipient acknowledgement used by
|
||||||
// group chats. The message row also carries the aggregate value used
|
// group chats. The message row also carries the aggregate value used
|
||||||
// by message bubbles and chat previews to leave the "sending" state.
|
// by message bubbles and chat previews to leave the "sending" state.
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,9 @@ pub(crate) async fn decorate_content(
|
||||||
};
|
};
|
||||||
|
|
||||||
content.sender_profile_counter = Some(config.avatar_counter);
|
content.sender_profile_counter = Some(config.avatar_counter);
|
||||||
|
// Announced on every content, including when it is off: a contact that
|
||||||
|
// switches the feature off has to stop receiving sealed envelopes.
|
||||||
|
content.sender_accepts_sealed_sender = Some(config.sealed_sender_enabled);
|
||||||
|
|
||||||
if config.ask_for_friend_promotions {
|
if config.ask_for_friend_promotions {
|
||||||
let database = ctx.app_db.read().await.clone();
|
let database = ctx.app_db.read().await.clone();
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ pub(crate) mod groups;
|
||||||
pub mod messages;
|
pub mod messages;
|
||||||
pub mod proto;
|
pub mod proto;
|
||||||
pub(super) mod runtime;
|
pub(super) mod runtime;
|
||||||
|
pub(crate) mod sealed_sender;
|
||||||
pub(super) mod server;
|
pub(super) mod server;
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,18 @@
|
||||||
syntax = "proto3";
|
syntax = "proto3";
|
||||||
package http_requests;
|
package http_requests;
|
||||||
|
|
||||||
|
message SealedSenderMessageRequest {
|
||||||
|
reserved 4;
|
||||||
|
int64 recipient_user_id = 1;
|
||||||
|
bytes sealed_sender_message = 2;
|
||||||
|
bytes privacy_pass_token = 3;
|
||||||
|
bool wake_receiver = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SealedSenderMessageResponse {
|
||||||
|
string message_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message UpdateGroupState {
|
message UpdateGroupState {
|
||||||
message UpdateTBS {
|
message UpdateTBS {
|
||||||
uint64 version_id = 1;
|
uint64 version_id = 1;
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@ message EncryptedContent {
|
||||||
optional int64 sender_profile_counter = 4;
|
optional int64 sender_profile_counter = 4;
|
||||||
optional bytes sender_user_discovery_version = 21;
|
optional bytes sender_user_discovery_version = 21;
|
||||||
optional bool ask_for_friend_promotions = 25;
|
optional bool ask_for_friend_promotions = 25;
|
||||||
|
/// Announces that this sender accepts sealed-sender envelopes. Peers must
|
||||||
|
/// keep using the named transport until they have seen this flag.
|
||||||
|
optional bool sender_accepts_sealed_sender = 28;
|
||||||
|
|
||||||
optional MessageUpdate message_update = 5;
|
optional MessageUpdate message_update = 5;
|
||||||
optional Media media = 6;
|
optional Media media = 6;
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ use crate::context::{Context, RuntimeMode};
|
||||||
use crate::error::{Result, TwonlyError};
|
use crate::error::{Result, TwonlyError};
|
||||||
use crate::services::groups::GroupService;
|
use crate::services::groups::GroupService;
|
||||||
use crate::services::mediafiles::MediaFileService;
|
use crate::services::mediafiles::MediaFileService;
|
||||||
|
use crate::services::privacy_pass::PrivacyPassTokens;
|
||||||
use prost::Message as ProstMessage;
|
use prost::Message as ProstMessage;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
|
|
@ -60,6 +61,12 @@ pub(crate) fn schedule_post_authentication(ctx: &Arc<Context>, in_background: bo
|
||||||
tracing::warn!("failed to replay legacy raw-byte outbox: {error}");
|
tracing::warn!("failed to replay legacy raw-byte outbox: {error}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Minting has to happen on an authenticated socket, so the pool is
|
||||||
|
// topped up while one is available rather than when a message needs it.
|
||||||
|
if let Err(error) = PrivacyPassTokens::refill_if_needed(&ctx).await {
|
||||||
|
tracing::warn!("failed to refill the Privacy Pass token pool: {error}");
|
||||||
|
}
|
||||||
|
|
||||||
if let Err(error) = messages::retransmit_queued_receipts(&ctx).await {
|
if let Err(error) = messages::retransmit_queued_receipts(&ctx).await {
|
||||||
tracing::warn!("failed to retransmit queued receipts: {error}");
|
tracing::warn!("failed to retransmit queued receipts: {error}");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
70
rust/src/api/sealed_sender.rs
Normal file
70
rust/src/api/sealed_sender.rs
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2026, Tobias Müller git@tsmr.eu
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
//! Anonymous upload endpoint for sealed-sender envelopes.
|
||||||
|
//!
|
||||||
|
//! Unlike every other API call this one carries no session and no
|
||||||
|
//! authentication header: the whole point is that the server must not learn who
|
||||||
|
//! sent the envelope. The Privacy Pass token in the request is what keeps the
|
||||||
|
//! endpoint from becoming an open relay.
|
||||||
|
|
||||||
|
use crate::api::proto::http_requests::{SealedSenderMessageRequest, SealedSenderMessageResponse};
|
||||||
|
use crate::bridge::api::RustApi;
|
||||||
|
use crate::error::{Result, TwonlyError};
|
||||||
|
use prost::Message as _;
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
|
/// One shared client, so a sealed upload reuses a pooled connection instead of
|
||||||
|
/// paying for a TLS handshake per message.
|
||||||
|
static CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
|
||||||
|
reqwest::Client::builder()
|
||||||
|
.timeout(std::time::Duration::from_secs(30))
|
||||||
|
.build()
|
||||||
|
.expect("the sealed-sender HTTP client must be constructible")
|
||||||
|
});
|
||||||
|
|
||||||
|
pub(crate) struct SealedSenderApi;
|
||||||
|
|
||||||
|
impl SealedSenderApi {
|
||||||
|
pub(crate) async fn upload(
|
||||||
|
recipient_user_id: i64,
|
||||||
|
sealed_sender_message: Vec<u8>,
|
||||||
|
privacy_pass_token: Vec<u8>,
|
||||||
|
wake_receiver: bool,
|
||||||
|
) -> Result<String> {
|
||||||
|
let request = SealedSenderMessageRequest {
|
||||||
|
recipient_user_id,
|
||||||
|
sealed_sender_message,
|
||||||
|
privacy_pass_token,
|
||||||
|
wake_receiver,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = CLIENT
|
||||||
|
.post(format!(
|
||||||
|
"{}sealed-sender/messages",
|
||||||
|
RustApi::api_base_url("https".into())
|
||||||
|
))
|
||||||
|
.header("Content-Type", "application/x-protobuf")
|
||||||
|
.body(request.encode_to_vec())
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|error| TwonlyError::Generic(error.to_string()))?;
|
||||||
|
|
||||||
|
let status = response.status();
|
||||||
|
if !status.is_success() {
|
||||||
|
return Err(TwonlyError::Generic(format!(
|
||||||
|
"sealed-sender upload returned {status}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = response
|
||||||
|
.bytes()
|
||||||
|
.await
|
||||||
|
.map_err(|error| TwonlyError::Generic(error.to_string()))?;
|
||||||
|
Ok(SealedSenderMessageResponse::decode(body)
|
||||||
|
.map_err(|error| TwonlyError::Generic(error.to_string()))?
|
||||||
|
.message_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ mod contacts;
|
||||||
mod memories;
|
mod memories;
|
||||||
mod passwordless;
|
mod passwordless;
|
||||||
pub mod prekeys;
|
pub mod prekeys;
|
||||||
|
mod privacy_pass;
|
||||||
mod purchases;
|
mod purchases;
|
||||||
mod transport;
|
mod transport;
|
||||||
|
|
||||||
|
|
|
||||||
56
rust/src/api/server/privacy_pass.rs
Normal file
56
rust/src/api/server/privacy_pass.rs
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2026, Tobias Müller git@tsmr.eu
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
use super::Server;
|
||||||
|
use crate::api::proto::client_to_server;
|
||||||
|
use crate::api::proto::server_to_client::response::ok::Ok as ResponseOk;
|
||||||
|
use crate::api::proto::server_to_client::response::PrivacyPassParameters;
|
||||||
|
use crate::api::runtime::helpers::decode_ok_value;
|
||||||
|
use crate::bridge::api::ServerResult;
|
||||||
|
use crate::context::Context;
|
||||||
|
use crate::error::Result;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
impl Server {
|
||||||
|
/// Loads the current issuance parameters: the daily token challenge, the
|
||||||
|
/// issuer public key and the limits the client has to stay inside.
|
||||||
|
pub(crate) async fn get_privacy_pass_parameters(
|
||||||
|
ctx: &Arc<Context>,
|
||||||
|
) -> Result<ServerResult<PrivacyPassParameters>> {
|
||||||
|
let bytes = Self::application(
|
||||||
|
ctx,
|
||||||
|
client_to_server::application_data::ApplicationData::GetPrivacyPassParameters(
|
||||||
|
client_to_server::application_data::GetPrivacyPassParameters {},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
decode_ok_value(bytes, |value| match value {
|
||||||
|
ResponseOk::PrivacyPassParameters(parameters) => Some(parameters),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Exchanges blinded token requests for the server's blinded evaluations.
|
||||||
|
///
|
||||||
|
/// This runs over the authenticated socket, so the server counts the tokens
|
||||||
|
/// against this account's daily quota. The tokens themselves are unlinkable
|
||||||
|
/// to it once they are finalized.
|
||||||
|
pub(crate) async fn issue_privacy_pass_tokens(
|
||||||
|
ctx: &Arc<Context>,
|
||||||
|
token_requests: Vec<Vec<u8>>,
|
||||||
|
) -> Result<ServerResult<Vec<Vec<u8>>>> {
|
||||||
|
let bytes = Self::application(
|
||||||
|
ctx,
|
||||||
|
client_to_server::application_data::ApplicationData::IssuePrivacyPassTokens(
|
||||||
|
client_to_server::application_data::IssuePrivacyPassTokens { token_requests },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
decode_ok_value(bytes, |value| match value {
|
||||||
|
ResponseOk::PrivacyPassTokenResponses(responses) => Some(responses.token_responses),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -45,6 +45,11 @@ pub struct Context {
|
||||||
mailbox_drained: Notify,
|
mailbox_drained: Notify,
|
||||||
incoming_generation: AtomicU64,
|
incoming_generation: AtomicU64,
|
||||||
incoming_committed: Notify,
|
incoming_committed: Notify,
|
||||||
|
/// Serializes Privacy Pass minting and holds the earliest time the issuer
|
||||||
|
/// may be asked again. It belongs to the account, not to the process: two
|
||||||
|
/// contexts in one process have separate quotas and must not block or back
|
||||||
|
/// each other off.
|
||||||
|
pub(crate) privacy_pass_issuance: Mutex<Option<std::time::Instant>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Context {
|
impl Context {
|
||||||
|
|
@ -121,6 +126,7 @@ impl Context {
|
||||||
mailbox_drained: Notify::new(),
|
mailbox_drained: Notify::new(),
|
||||||
incoming_generation: AtomicU64::new(0),
|
incoming_generation: AtomicU64::new(0),
|
||||||
incoming_committed: Notify::new(),
|
incoming_committed: Notify::new(),
|
||||||
|
privacy_pass_issuance: Mutex::new(None),
|
||||||
});
|
});
|
||||||
ApiRuntime::initialize(&ctx).await?;
|
ApiRuntime::initialize(&ctx).await?;
|
||||||
ApiRuntime::connect(&ctx).await?;
|
ApiRuntime::connect(&ctx).await?;
|
||||||
|
|
@ -280,6 +286,7 @@ impl Context {
|
||||||
mailbox_drained: Notify::new(),
|
mailbox_drained: Notify::new(),
|
||||||
incoming_generation: AtomicU64::new(0),
|
incoming_generation: AtomicU64::new(0),
|
||||||
incoming_committed: Notify::new(),
|
incoming_committed: Notify::new(),
|
||||||
|
privacy_pass_issuance: Mutex::new(None),
|
||||||
});
|
});
|
||||||
if let Err(error) = ctx.initialize_user_discovery_from_config().await {
|
if let Err(error) = ctx.initialize_user_discovery_from_config().await {
|
||||||
tracing::warn!("failed to initialize user discovery: {error}");
|
tracing::warn!("failed to initialize user discovery: {error}");
|
||||||
|
|
@ -320,6 +327,7 @@ impl Context {
|
||||||
mailbox_drained: Notify::new(),
|
mailbox_drained: Notify::new(),
|
||||||
incoming_generation: AtomicU64::new(0),
|
incoming_generation: AtomicU64::new(0),
|
||||||
incoming_committed: Notify::new(),
|
incoming_committed: Notify::new(),
|
||||||
|
privacy_pass_issuance: Mutex::new(None),
|
||||||
});
|
});
|
||||||
if let Err(error) = ctx.initialize_user_discovery_from_config().await {
|
if let Err(error) = ctx.initialize_user_discovery_from_config().await {
|
||||||
tracing::warn!("failed to initialize user discovery: {error}");
|
tracing::warn!("failed to initialize user discovery: {error}");
|
||||||
|
|
|
||||||
16
rust/src/database/app/migrations/0004_sealed_sender.sql
Normal file
16
rust/src/database/app/migrations/0004_sealed_sender.sql
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
-- Peers announce sealed-sender support in every encrypted content they send.
|
||||||
|
-- Until a contact has announced it, messages keep using the named transport.
|
||||||
|
ALTER TABLE contacts
|
||||||
|
ADD COLUMN sealed_sender_enabled INTEGER NOT NULL DEFAULT 0
|
||||||
|
CHECK (sealed_sender_enabled IN (0, 1));
|
||||||
|
|
||||||
|
-- Anonymous rate-limiting tokens for the sealed-sender upload endpoint. They
|
||||||
|
-- are issued over the authenticated WebSocket and spent unauthenticated over
|
||||||
|
-- HTTP, so a token must never be linkable back to its issuance.
|
||||||
|
CREATE TABLE privacy_pass_tokens (
|
||||||
|
token BLOB NOT NULL PRIMARY KEY,
|
||||||
|
expires_at INTEGER NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_privacy_pass_tokens_expires_at ON privacy_pass_tokens(expires_at);
|
||||||
|
|
@ -38,6 +38,7 @@ pub struct Contact {
|
||||||
pub ask_for_friend_promotions: Option<i64>,
|
pub ask_for_friend_promotions: Option<i64>,
|
||||||
pub media_send_counter: i64,
|
pub media_send_counter: i64,
|
||||||
pub media_received_counter: i64,
|
pub media_received_counter: i64,
|
||||||
|
pub sealed_sender_enabled: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(bon::Builder)]
|
#[derive(bon::Builder)]
|
||||||
|
|
@ -182,6 +183,27 @@ impl Contact {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Records whether a contact announced that it accepts sealed-sender
|
||||||
|
/// envelopes. A peer that stops announcing it goes back to named sends.
|
||||||
|
pub async fn set_sealed_sender_enabled(
|
||||||
|
t: &mut Transaction<'_, Sqlite>,
|
||||||
|
user_id: i64,
|
||||||
|
enabled: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
// Announced on every encrypted content, so the write is skipped unless
|
||||||
|
// the value actually changes.
|
||||||
|
sqlx::query!(
|
||||||
|
r#"UPDATE contacts SET sealed_sender_enabled = ?
|
||||||
|
WHERE user_id = ? AND sealed_sender_enabled != ?"#,
|
||||||
|
enabled,
|
||||||
|
user_id,
|
||||||
|
enabled,
|
||||||
|
)
|
||||||
|
.execute(&mut **t)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn is_user_discovery_allowed(
|
pub async fn is_user_discovery_allowed(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
t: &mut Transaction<'_, Sqlite>,
|
t: &mut Transaction<'_, Sqlite>,
|
||||||
|
|
|
||||||
|
|
@ -5603,6 +5603,7 @@ impl SseDecode for crate::user_config::UserConfig {
|
||||||
let mut var_storeMediaFilesInGallery = <bool>::sse_decode(deserializer);
|
let mut var_storeMediaFilesInGallery = <bool>::sse_decode(deserializer);
|
||||||
let mut var_autoStoreAllSendUnlimitedMediaFiles = <bool>::sse_decode(deserializer);
|
let mut var_autoStoreAllSendUnlimitedMediaFiles = <bool>::sse_decode(deserializer);
|
||||||
let mut var_typingIndicators = <bool>::sse_decode(deserializer);
|
let mut var_typingIndicators = <bool>::sse_decode(deserializer);
|
||||||
|
let mut var_sealedSenderEnabled = <bool>::sse_decode(deserializer);
|
||||||
let mut var_showRestoreFlame = <bool>::sse_decode(deserializer);
|
let mut var_showRestoreFlame = <bool>::sse_decode(deserializer);
|
||||||
let mut var_myBestFriendGroupId = <Option<String>>::sse_decode(deserializer);
|
let mut var_myBestFriendGroupId = <Option<String>>::sse_decode(deserializer);
|
||||||
let mut var_signalLastSignedPreKeyUpdated =
|
let mut var_signalLastSignedPreKeyUpdated =
|
||||||
|
|
@ -5668,6 +5669,7 @@ impl SseDecode for crate::user_config::UserConfig {
|
||||||
store_media_files_in_gallery: var_storeMediaFilesInGallery,
|
store_media_files_in_gallery: var_storeMediaFilesInGallery,
|
||||||
auto_store_all_send_unlimited_media_files: var_autoStoreAllSendUnlimitedMediaFiles,
|
auto_store_all_send_unlimited_media_files: var_autoStoreAllSendUnlimitedMediaFiles,
|
||||||
typing_indicators: var_typingIndicators,
|
typing_indicators: var_typingIndicators,
|
||||||
|
sealed_sender_enabled: var_sealedSenderEnabled,
|
||||||
show_restore_flame: var_showRestoreFlame,
|
show_restore_flame: var_showRestoreFlame,
|
||||||
my_best_friend_group_id: var_myBestFriendGroupId,
|
my_best_friend_group_id: var_myBestFriendGroupId,
|
||||||
signal_last_signed_pre_key_updated: var_signalLastSignedPreKeyUpdated,
|
signal_last_signed_pre_key_updated: var_signalLastSignedPreKeyUpdated,
|
||||||
|
|
@ -6653,6 +6655,7 @@ impl flutter_rust_bridge::IntoDart for crate::user_config::UserConfig {
|
||||||
.into_into_dart()
|
.into_into_dart()
|
||||||
.into_dart(),
|
.into_dart(),
|
||||||
self.typing_indicators.into_into_dart().into_dart(),
|
self.typing_indicators.into_into_dart().into_dart(),
|
||||||
|
self.sealed_sender_enabled.into_into_dart().into_dart(),
|
||||||
self.show_restore_flame.into_into_dart().into_dart(),
|
self.show_restore_flame.into_into_dart().into_dart(),
|
||||||
self.my_best_friend_group_id.into_into_dart().into_dart(),
|
self.my_best_friend_group_id.into_into_dart().into_dart(),
|
||||||
self.signal_last_signed_pre_key_updated
|
self.signal_last_signed_pre_key_updated
|
||||||
|
|
@ -7562,6 +7565,7 @@ impl SseEncode for crate::user_config::UserConfig {
|
||||||
<bool>::sse_encode(self.store_media_files_in_gallery, serializer);
|
<bool>::sse_encode(self.store_media_files_in_gallery, serializer);
|
||||||
<bool>::sse_encode(self.auto_store_all_send_unlimited_media_files, serializer);
|
<bool>::sse_encode(self.auto_store_all_send_unlimited_media_files, serializer);
|
||||||
<bool>::sse_encode(self.typing_indicators, serializer);
|
<bool>::sse_encode(self.typing_indicators, serializer);
|
||||||
|
<bool>::sse_encode(self.sealed_sender_enabled, serializer);
|
||||||
<bool>::sse_encode(self.show_restore_flame, serializer);
|
<bool>::sse_encode(self.show_restore_flame, serializer);
|
||||||
<Option<String>>::sse_encode(self.my_best_friend_group_id, serializer);
|
<Option<String>>::sse_encode(self.my_best_friend_group_id, serializer);
|
||||||
<Option<chrono::DateTime<chrono::Utc>>>::sse_encode(
|
<Option<chrono::DateTime<chrono::Utc>>>::sse_encode(
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,6 @@ pub enum SealedSenderError {
|
||||||
pub(crate) struct SealedSender;
|
pub(crate) struct SealedSender;
|
||||||
|
|
||||||
impl SealedSender {
|
impl SealedSender {
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) fn encrypt<R>(
|
pub(crate) fn encrypt<R>(
|
||||||
from_user_id: i64,
|
from_user_id: i64,
|
||||||
recipient_user_id: i64,
|
recipient_user_id: i64,
|
||||||
|
|
@ -107,7 +106,6 @@ impl SealedSender {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn encrypt_at<R>(
|
fn encrypt_at<R>(
|
||||||
from_user_id: i64,
|
from_user_id: i64,
|
||||||
recipient_user_id: i64,
|
recipient_user_id: i64,
|
||||||
|
|
|
||||||
|
|
@ -8,3 +8,5 @@ pub mod groups;
|
||||||
pub mod mediafiles;
|
pub mod mediafiles;
|
||||||
pub mod messages;
|
pub mod messages;
|
||||||
pub mod notifications;
|
pub mod notifications;
|
||||||
|
pub mod privacy_pass;
|
||||||
|
pub mod sealed_sender;
|
||||||
|
|
|
||||||
276
rust/src/services/privacy_pass.rs
Normal file
276
rust/src/services/privacy_pass.rs
Normal file
|
|
@ -0,0 +1,276 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2026, Tobias Müller git@tsmr.eu
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
//! Client side of the Privacy Pass token pool that rate-limits sealed-sender
|
||||||
|
//! uploads.
|
||||||
|
//!
|
||||||
|
//! Tokens are minted over the authenticated WebSocket, where the server counts
|
||||||
|
//! them against this account's daily quota, and spent later on the anonymous
|
||||||
|
//! HTTP upload endpoint. Because the issued token is unblinded locally, the
|
||||||
|
//! server cannot link a redeemed token back to the account it was issued to,
|
||||||
|
//! which is what lets the upload stay unauthenticated without becoming an open
|
||||||
|
//! relay.
|
||||||
|
|
||||||
|
use crate::api::Server;
|
||||||
|
use crate::bridge::api::ServerResult;
|
||||||
|
use crate::context::Context;
|
||||||
|
use crate::error::{Result, TwonlyError};
|
||||||
|
use crate::user_config::UserConfig;
|
||||||
|
use p384::NistP384;
|
||||||
|
use privacypass::auth::authenticate::TokenChallenge;
|
||||||
|
use privacypass::common::private::deserialize_public_key;
|
||||||
|
use privacypass::private_tokens::{TokenRequest, TokenResponse, TokenState};
|
||||||
|
use privacypass::Serialize as PrivacyPassSerialize;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
/// Refill once the pool can no longer cover a short burst of messages.
|
||||||
|
const REFILL_THRESHOLD: i64 = 5;
|
||||||
|
/// Never ask for more than this, however large the server's batch limit is.
|
||||||
|
/// Every token in a batch costs a blind and an unblind, both P-384 scalar
|
||||||
|
/// multiplications, so the batch size is what bounds the CPU spike a refill
|
||||||
|
/// puts on the device.
|
||||||
|
const MAX_TOKENS_PER_REFILL: usize = 10;
|
||||||
|
/// Stop using a token slightly before the server would reject it, so a message
|
||||||
|
/// in flight around midnight UTC is not refused.
|
||||||
|
const EXPIRY_SAFETY_MARGIN_SECONDS: i64 = 5 * 60;
|
||||||
|
/// Back off this long after the server refused to issue tokens.
|
||||||
|
const REFUSED_ISSUANCE_BACKOFF: Duration = Duration::from_secs(15 * 60);
|
||||||
|
|
||||||
|
pub(crate) struct PrivacyPassTokens;
|
||||||
|
|
||||||
|
impl PrivacyPassTokens {
|
||||||
|
/// Removes one unexpired token from the local pool.
|
||||||
|
///
|
||||||
|
/// A token is spent by handing it to the server, so it is deleted before it
|
||||||
|
/// is used: replaying it would be rejected anyway, and keeping it would
|
||||||
|
/// stall every later message behind the same dead token.
|
||||||
|
///
|
||||||
|
/// Minting never happens here. It needs two server round trips, and putting
|
||||||
|
/// those on the send path would delay the very message that emptied the
|
||||||
|
/// pool; an empty pool simply means this message goes out named while the
|
||||||
|
/// refill runs behind it.
|
||||||
|
pub(crate) async fn take(ctx: &Arc<Context>) -> Result<Option<Vec<u8>>> {
|
||||||
|
let token = Self::pop(ctx).await?;
|
||||||
|
Self::spawn_refill(ctx);
|
||||||
|
Ok(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tops the pool up when it is running low. Safe to call on every
|
||||||
|
/// connection: it is a no-op while enough tokens are left.
|
||||||
|
pub(crate) async fn refill_if_needed(ctx: &Arc<Context>) -> Result<()> {
|
||||||
|
// Tokens are only ever spent on sealed-sender uploads. Minting them for
|
||||||
|
// an account that has the feature off would burn its daily quota on
|
||||||
|
// tokens it can never use.
|
||||||
|
if !UserConfig::load_from(ctx)?.is_some_and(|config| config.sealed_sender_enabled) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if Self::count(ctx).await? >= REFILL_THRESHOLD {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Self::refill(ctx).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn spawn_refill(ctx: &Arc<Context>) {
|
||||||
|
let ctx = ctx.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(error) = Self::refill_if_needed(&ctx).await {
|
||||||
|
tracing::warn!("Privacy Pass refill failed: {error}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn count(ctx: &Arc<Context>) -> Result<i64> {
|
||||||
|
let database = ctx.app_db.read().await.clone();
|
||||||
|
let now = now_seconds();
|
||||||
|
Ok(sqlx::query_scalar!(
|
||||||
|
"SELECT COUNT(*) FROM privacy_pass_tokens WHERE expires_at > ?",
|
||||||
|
now
|
||||||
|
)
|
||||||
|
.fetch_one(&database.pool)
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn pop(ctx: &Arc<Context>) -> Result<Option<Vec<u8>>> {
|
||||||
|
let database = ctx.app_db.read().await.clone();
|
||||||
|
let now = now_seconds();
|
||||||
|
let mut transaction = database.pool.begin().await?;
|
||||||
|
sqlx::query!("DELETE FROM privacy_pass_tokens WHERE expires_at <= ?", now)
|
||||||
|
.execute(&mut *transaction)
|
||||||
|
.await?;
|
||||||
|
let token = sqlx::query_scalar!(
|
||||||
|
r#"DELETE FROM privacy_pass_tokens
|
||||||
|
WHERE token = (
|
||||||
|
SELECT token FROM privacy_pass_tokens
|
||||||
|
WHERE expires_at > ?
|
||||||
|
ORDER BY expires_at ASC
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
RETURNING token"#,
|
||||||
|
now
|
||||||
|
)
|
||||||
|
.fetch_optional(&mut *transaction)
|
||||||
|
.await?;
|
||||||
|
transaction.commit().await?;
|
||||||
|
Ok(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn refill(ctx: &Arc<Context>) -> Result<()> {
|
||||||
|
// A refill already in flight will fill the pool for everyone waiting on
|
||||||
|
// it, so a second one would only spend quota twice.
|
||||||
|
let Ok(mut issuance) = ctx.privacy_pass_issuance.try_lock() else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
if issuance.is_some_and(|next| Instant::now() < next) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
// Another caller may have filled the pool while this one waited.
|
||||||
|
if Self::count(ctx).await? >= REFILL_THRESHOLD {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let parameters = match Server::get_privacy_pass_parameters(ctx).await? {
|
||||||
|
ServerResult::Ok(parameters) => parameters,
|
||||||
|
ServerResult::ErrorCode(code) => {
|
||||||
|
*issuance = Some(Instant::now() + REFUSED_ISSUANCE_BACKOFF);
|
||||||
|
return Err(TwonlyError::Generic(format!(
|
||||||
|
"server rejected the Privacy Pass parameter request with code {code}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let challenge = TokenChallenge::deserialize(parameters.token_challenge.as_slice())
|
||||||
|
.map_err(|error| {
|
||||||
|
TwonlyError::Generic(format!("invalid Privacy Pass challenge: {error}"))
|
||||||
|
})?;
|
||||||
|
let public_key =
|
||||||
|
deserialize_public_key::<NistP384>(¶meters.public_key).map_err(|error| {
|
||||||
|
TwonlyError::Generic(format!("invalid Privacy Pass public key: {error}"))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let batch_size = (parameters.max_batch_size as usize).min(MAX_TOKENS_PER_REFILL);
|
||||||
|
if batch_size == 0 {
|
||||||
|
return Err(TwonlyError::Generic(
|
||||||
|
"server does not issue any Privacy Pass tokens".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blinding a batch is a run of P-384 scalar multiplications. On the
|
||||||
|
// async runtime it would stall the socket tasks sharing those threads,
|
||||||
|
// which is felt as dropped connections rather than as slow minting.
|
||||||
|
let (requests, states) = tokio::task::spawn_blocking(move || {
|
||||||
|
let mut requests = Vec::with_capacity(batch_size);
|
||||||
|
let mut states: Vec<TokenState<NistP384>> = Vec::with_capacity(batch_size);
|
||||||
|
for _ in 0..batch_size {
|
||||||
|
let (request, state) = TokenRequest::<NistP384>::new(public_key, &challenge)
|
||||||
|
.map_err(|error| {
|
||||||
|
TwonlyError::Generic(format!(
|
||||||
|
"could not blind a Privacy Pass token: {error}"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
requests.push(request.tls_serialize_detached().map_err(|error| {
|
||||||
|
TwonlyError::Generic(format!("could not serialize a token request: {error}"))
|
||||||
|
})?);
|
||||||
|
states.push(state);
|
||||||
|
}
|
||||||
|
Ok::<_, TwonlyError>((requests, states))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|error| TwonlyError::Generic(format!("token blinding panicked: {error}")))??;
|
||||||
|
|
||||||
|
let responses = match Server::issue_privacy_pass_tokens(ctx, requests).await? {
|
||||||
|
ServerResult::Ok(responses) => responses,
|
||||||
|
ServerResult::ErrorCode(code) => {
|
||||||
|
*issuance = Some(Instant::now() + REFUSED_ISSUANCE_BACKOFF);
|
||||||
|
return Err(TwonlyError::Generic(format!(
|
||||||
|
"server rejected the Privacy Pass issuance with code {code}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if responses.len() != states.len() {
|
||||||
|
return Err(TwonlyError::Generic(
|
||||||
|
"server returned the wrong number of Privacy Pass responses".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unblinding is the same kind of curve work, so it is offloaded too.
|
||||||
|
let tokens = tokio::task::spawn_blocking(move || {
|
||||||
|
responses
|
||||||
|
.into_iter()
|
||||||
|
.zip(states.iter())
|
||||||
|
.map(|(serialized, state)| {
|
||||||
|
let response = TokenResponse::<NistP384>::try_from_bytes(&serialized).map_err(
|
||||||
|
|error| {
|
||||||
|
TwonlyError::Generic(format!("invalid Privacy Pass response: {error}"))
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
response
|
||||||
|
.issue_token(state)
|
||||||
|
.map_err(|error| {
|
||||||
|
TwonlyError::Generic(format!(
|
||||||
|
"could not finalize a Privacy Pass token: {error}"
|
||||||
|
))
|
||||||
|
})?
|
||||||
|
.tls_serialize_detached()
|
||||||
|
.map_err(|error| {
|
||||||
|
TwonlyError::Generic(format!(
|
||||||
|
"could not serialize a Privacy Pass token: {error}"
|
||||||
|
))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<Vec<u8>>>>()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|error| TwonlyError::Generic(format!("token finalization panicked: {error}")))??;
|
||||||
|
|
||||||
|
let expires_at = expiry_for_todays_challenge(i64::from(parameters.max_age_seconds));
|
||||||
|
let database = ctx.app_db.read().await.clone();
|
||||||
|
let mut transaction = database.pool.begin().await?;
|
||||||
|
for token in tokens {
|
||||||
|
sqlx::query!(
|
||||||
|
r#"INSERT INTO privacy_pass_tokens(token, expires_at) VALUES (?, ?)
|
||||||
|
ON CONFLICT(token) DO NOTHING"#,
|
||||||
|
token,
|
||||||
|
expires_at,
|
||||||
|
)
|
||||||
|
.execute(&mut *transaction)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
transaction.commit().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn now_seconds() -> i64 {
|
||||||
|
chrono::Utc::now().timestamp()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The server derives its challenge from the current UTC day and accepts a
|
||||||
|
/// token for `max_age_seconds` counted from the start of that day, not from the
|
||||||
|
/// moment it was issued.
|
||||||
|
fn expiry_for_todays_challenge(max_age_seconds: i64) -> i64 {
|
||||||
|
const SECONDS_PER_DAY: i64 = 24 * 60 * 60;
|
||||||
|
let start_of_day = now_seconds().div_euclid(SECONDS_PER_DAY) * SECONDS_PER_DAY;
|
||||||
|
start_of_day + max_age_seconds - EXPIRY_SAFETY_MARGIN_SECONDS
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tokens_expire_with_the_day_they_were_issued_for() {
|
||||||
|
const SECONDS_PER_DAY: i64 = 24 * 60 * 60;
|
||||||
|
let expiry = expiry_for_todays_challenge(7 * SECONDS_PER_DAY);
|
||||||
|
let start_of_day = now_seconds().div_euclid(SECONDS_PER_DAY) * SECONDS_PER_DAY;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
expiry,
|
||||||
|
start_of_day + 7 * SECONDS_PER_DAY - EXPIRY_SAFETY_MARGIN_SECONDS
|
||||||
|
);
|
||||||
|
assert!(expiry > now_seconds());
|
||||||
|
}
|
||||||
|
}
|
||||||
216
rust/src/services/sealed_sender.rs
Normal file
216
rust/src/services/sealed_sender.rs
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2026, Tobias Müller git@tsmr.eu
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
//! Chooses between the sealed and the named transport for an outgoing message.
|
||||||
|
//!
|
||||||
|
//! A sealed envelope is uploaded anonymously, so the server never learns who
|
||||||
|
//! sent it. That only works when both sides agree: the recipient has to
|
||||||
|
//! understand the envelope, and this account has to have the feature enabled.
|
||||||
|
//! Everything else — an unknown identity key, an empty token pool, a failing
|
||||||
|
//! upload — falls back to the named transport rather than dropping the message.
|
||||||
|
|
||||||
|
use crate::api::messages::incoming::messages::PreparedQueuedReceipt;
|
||||||
|
use crate::api::sealed_sender::SealedSenderApi;
|
||||||
|
use crate::context::Context;
|
||||||
|
use crate::error::{Result, TwonlyError};
|
||||||
|
use crate::sealed_sender::SealedSender;
|
||||||
|
use crate::services::privacy_pass::PrivacyPassTokens;
|
||||||
|
use crate::user_config::UserConfig;
|
||||||
|
use libsignal_protocol::IdentityKeyPair;
|
||||||
|
use rand::SeedableRng;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub(crate) struct SealedSenderService;
|
||||||
|
|
||||||
|
impl SealedSenderService {
|
||||||
|
/// Whether messages to this contact are allowed to travel sealed.
|
||||||
|
///
|
||||||
|
/// The contact's flag is only set once they have announced support in an
|
||||||
|
/// encrypted content of their own, so a peer that does not understand the
|
||||||
|
/// envelope never receives one.
|
||||||
|
pub(crate) async fn is_enabled_for(ctx: &Arc<Context>, contact_id: i64) -> Result<bool> {
|
||||||
|
// The contact's flag is checked first on purpose. This runs for every
|
||||||
|
// queued receipt, and reading the user configuration means a file read
|
||||||
|
// and a JSON parse under a process-wide lock, which is far more
|
||||||
|
// expensive than an indexed lookup on the primary key.
|
||||||
|
let database = ctx.app_db.read().await.clone();
|
||||||
|
let contact_accepts = sqlx::query_scalar!(
|
||||||
|
"SELECT EXISTS(SELECT 1 FROM contacts WHERE user_id = ? AND sealed_sender_enabled = 1)",
|
||||||
|
contact_id,
|
||||||
|
)
|
||||||
|
.fetch_one(&database.pool)
|
||||||
|
.await?
|
||||||
|
!= 0;
|
||||||
|
if !contact_accepts {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
Ok(UserConfig::load_from(ctx)?.is_some_and(|config| config.sealed_sender_enabled))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tries to deliver a prepared receipt as a sealed envelope.
|
||||||
|
///
|
||||||
|
/// Returns `false` when the message has to go out over the named transport
|
||||||
|
/// instead. The caller may then send it normally: a receiver that ends up
|
||||||
|
/// seeing both copies discards the second one by receipt ID.
|
||||||
|
pub(crate) async fn try_send(
|
||||||
|
ctx: &Arc<Context>,
|
||||||
|
receipt: &PreparedQueuedReceipt,
|
||||||
|
) -> Result<bool> {
|
||||||
|
if !Self::is_enabled_for(ctx, receipt.contact_id).await? {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(recipient_identity) = ctx.get_identity(receipt.contact_id).await? else {
|
||||||
|
tracing::info!(
|
||||||
|
contact_id = receipt.contact_id,
|
||||||
|
"no pinned identity key yet; sending this message named"
|
||||||
|
);
|
||||||
|
return Ok(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
let (from_user_id, sender_identity) = {
|
||||||
|
let key_manager = ctx.key_manager.lock().await;
|
||||||
|
let Some(from_user_id) = key_manager.user_id else {
|
||||||
|
return Ok(false);
|
||||||
|
};
|
||||||
|
let Some(identity) = key_manager.signal_identity.as_ref() else {
|
||||||
|
return Ok(false);
|
||||||
|
};
|
||||||
|
let identity =
|
||||||
|
IdentityKeyPair::try_from(identity.identity_key_pair_structure.as_slice())
|
||||||
|
.map_err(|error| TwonlyError::Signal(error.to_string()))?;
|
||||||
|
(from_user_id, identity)
|
||||||
|
};
|
||||||
|
|
||||||
|
// An empty pool, or an issuer that refuses to mint, must never hold a
|
||||||
|
// message back: it just goes out named instead.
|
||||||
|
let token = match PrivacyPassTokens::take(ctx).await {
|
||||||
|
Ok(Some(token)) => token,
|
||||||
|
Ok(None) => {
|
||||||
|
tracing::info!("no Privacy Pass token available; sending this message named");
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
tracing::warn!("could not obtain a Privacy Pass token: {error}");
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let envelope = {
|
||||||
|
let mut rng = rand::rngs::StdRng::from_os_rng();
|
||||||
|
SealedSender::encrypt(
|
||||||
|
from_user_id,
|
||||||
|
receipt.contact_id,
|
||||||
|
receipt.message.clone(),
|
||||||
|
&sender_identity,
|
||||||
|
recipient_identity.public_key(),
|
||||||
|
&mut rng,
|
||||||
|
)?
|
||||||
|
};
|
||||||
|
|
||||||
|
match SealedSenderApi::upload(receipt.contact_id, envelope, token, receipt.wake_receiver)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(message_id) => {
|
||||||
|
tracing::info!(
|
||||||
|
receipt_id = receipt.receipt_id,
|
||||||
|
message_id,
|
||||||
|
"sent message via sealed sender"
|
||||||
|
);
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
tracing::warn!("sealed-sender upload failed, falling back to named send: {error}");
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::database::app::tables::Contact;
|
||||||
|
|
||||||
|
async fn context(
|
||||||
|
sealed_sender_enabled: bool,
|
||||||
|
) -> anyhow::Result<(tempfile::TempDir, Arc<Context>)> {
|
||||||
|
let temp = tempfile::tempdir()?;
|
||||||
|
let data_dir = temp.path().join("data");
|
||||||
|
std::fs::create_dir_all(data_dir.join("keyvalue"))?;
|
||||||
|
let config = UserConfig {
|
||||||
|
sealed_sender_enabled,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
std::fs::write(
|
||||||
|
data_dir.join("keyvalue/user.json"),
|
||||||
|
serde_json::to_vec(&config)?,
|
||||||
|
)?;
|
||||||
|
let context = Context::init_for_testing(temp.path().join("database"), data_dir).await?;
|
||||||
|
Ok((temp, context))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn insert_contact(ctx: &Arc<Context>, user_id: i64) -> anyhow::Result<()> {
|
||||||
|
let database = ctx.app_db.read().await.clone();
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT INTO contacts(user_id, username, accepted) VALUES (?, ?, 1)",
|
||||||
|
user_id,
|
||||||
|
"peer",
|
||||||
|
)
|
||||||
|
.execute(&database.pool)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn announce(ctx: &Arc<Context>, user_id: i64, enabled: bool) -> anyhow::Result<()> {
|
||||||
|
let database = ctx.app_db.read().await.clone();
|
||||||
|
let mut transaction = database.pool.begin().await?;
|
||||||
|
Contact::set_sealed_sender_enabled(&mut transaction, user_id, enabled).await?;
|
||||||
|
transaction.commit().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn a_contact_that_never_announced_support_keeps_the_named_transport() -> anyhow::Result<()>
|
||||||
|
{
|
||||||
|
let (_temp, ctx) = context(true).await?;
|
||||||
|
insert_contact(&ctx, 21).await?;
|
||||||
|
|
||||||
|
assert!(!SealedSenderService::is_enabled_for(&ctx, 21).await?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn both_sides_have_to_enable_the_feature() -> anyhow::Result<()> {
|
||||||
|
let (_temp, ctx) = context(true).await?;
|
||||||
|
insert_contact(&ctx, 21).await?;
|
||||||
|
announce(&ctx, 21, true).await?;
|
||||||
|
|
||||||
|
assert!(SealedSenderService::is_enabled_for(&ctx, 21).await?);
|
||||||
|
|
||||||
|
// The contact withdrawing its announcement is enough to stop sealing.
|
||||||
|
announce(&ctx, 21, false).await?;
|
||||||
|
assert!(!SealedSenderService::is_enabled_for(&ctx, 21).await?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn the_local_setting_disables_sealing_for_every_contact() -> anyhow::Result<()> {
|
||||||
|
let (_temp, ctx) = context(false).await?;
|
||||||
|
insert_contact(&ctx, 21).await?;
|
||||||
|
announce(&ctx, 21, true).await?;
|
||||||
|
|
||||||
|
assert!(!SealedSenderService::is_enabled_for(&ctx, 21).await?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn an_unknown_contact_is_never_sealed() -> anyhow::Result<()> {
|
||||||
|
let (_temp, ctx) = context(true).await?;
|
||||||
|
|
||||||
|
assert!(!SealedSenderService::is_enabled_for(&ctx, 404).await?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -248,6 +248,12 @@ pub struct UserConfig {
|
||||||
#[serde(default = "defaults::true_value")]
|
#[serde(default = "defaults::true_value")]
|
||||||
#[frb(non_final)]
|
#[frb(non_final)]
|
||||||
pub typing_indicators: bool,
|
pub typing_indicators: bool,
|
||||||
|
/// Announces to contacts that this account accepts sealed-sender envelopes,
|
||||||
|
/// and lets this account send them. Both sides have to have it on before
|
||||||
|
/// anything travels sealed.
|
||||||
|
#[serde(default = "defaults::true_value")]
|
||||||
|
#[frb(non_final)]
|
||||||
|
pub sealed_sender_enabled: bool,
|
||||||
#[serde(default = "defaults::true_value")]
|
#[serde(default = "defaults::true_value")]
|
||||||
#[frb(non_final)]
|
#[frb(non_final)]
|
||||||
pub show_restore_flame: bool,
|
pub show_restore_flame: bool,
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ mod media;
|
||||||
mod notifications;
|
mod notifications;
|
||||||
#[path = "api/recovery.rs"]
|
#[path = "api/recovery.rs"]
|
||||||
mod recovery;
|
mod recovery;
|
||||||
|
#[path = "api/sealed_sender.rs"]
|
||||||
|
mod sealed_sender;
|
||||||
#[path = "api/server_api.rs"]
|
#[path = "api/server_api.rs"]
|
||||||
mod server_api;
|
mod server_api;
|
||||||
#[path = "api/session_recovery.rs"]
|
#[path = "api/session_recovery.rs"]
|
||||||
|
|
|
||||||
109
rust/tests/api/sealed_sender.rs
Normal file
109
rust/tests/api/sealed_sender.rs
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
use crate::tester::{init_tracing, Tester};
|
||||||
|
use rust_lib_twonly::bridge::api::ApiConnectionState;
|
||||||
|
use rust_lib_twonly::database::app::tables::Group;
|
||||||
|
use rust_lib_twonly::services::contacts::ContactService;
|
||||||
|
use rust_lib_twonly::services::messages::MessageService;
|
||||||
|
|
||||||
|
async fn ready_tester() -> anyhow::Result<Tester> {
|
||||||
|
let mut tester = Tester::new().await?;
|
||||||
|
tester.set_sealed_sender_enabled(true)?;
|
||||||
|
tester.wait_until(ApiConnectionState::Connected).await?;
|
||||||
|
tester.register_and_authenticate().await?;
|
||||||
|
tester.wait_until(ApiConnectionState::Authenticated).await?;
|
||||||
|
Ok(tester)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Exercises the whole sealed-sender path against the running dev server:
|
||||||
|
/// Privacy Pass issuance, the capability announcement that gates it, the
|
||||||
|
/// anonymous upload, and delivery back to the recipient.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_sealed_sender_end_to_end() -> anyhow::Result<()> {
|
||||||
|
init_tracing();
|
||||||
|
|
||||||
|
let tester_a = ready_tester().await?;
|
||||||
|
let tester_b = ready_tester().await?;
|
||||||
|
tracing::info!(
|
||||||
|
sender = tester_a.user_id,
|
||||||
|
recipient = tester_b.user_id,
|
||||||
|
"Testers are ready"
|
||||||
|
);
|
||||||
|
|
||||||
|
let group_id = Group::direct_chat_id(tester_a.user_id, tester_b.user_id);
|
||||||
|
|
||||||
|
// Tokens are minted right after authentication, so a message never has to
|
||||||
|
// wait for a round trip before it can be sealed.
|
||||||
|
tester_a.wait_for_privacy_pass_tokens().await?;
|
||||||
|
|
||||||
|
ContactService::new(&tester_a.context)
|
||||||
|
.request_by_username(tester_b.username.clone(), true)
|
||||||
|
.await?;
|
||||||
|
tester_b
|
||||||
|
.wait_for_contact_state(tester_a.user_id, false, true)
|
||||||
|
.await?;
|
||||||
|
ContactService::new(&tester_b.context)
|
||||||
|
.accept_request(tester_a.user_id, true)
|
||||||
|
.await?;
|
||||||
|
tester_a
|
||||||
|
.wait_for_contact_state(tester_b.user_id, true, false)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// The contact request and its acceptance each carry the announcement, so
|
||||||
|
// by now both sides know the other accepts sealed envelopes.
|
||||||
|
tester_a
|
||||||
|
.wait_for_contact_sealed_sender(tester_b.user_id, true)
|
||||||
|
.await?;
|
||||||
|
tester_b
|
||||||
|
.wait_for_contact_sealed_sender(tester_a.user_id, true)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// A sealed message arrives like any other, but the server never sees who
|
||||||
|
// sent it.
|
||||||
|
let sealed_id = MessageService::new(&tester_a.context)
|
||||||
|
.insert_and_send_text(group_id.clone(), "Sealed hello".into(), None)
|
||||||
|
.await?;
|
||||||
|
tester_b
|
||||||
|
.wait_for_text_message(&sealed_id, tester_a.user_id, "Sealed hello")
|
||||||
|
.await?;
|
||||||
|
tester_a.wait_for_message_ack_by_server(&sealed_id).await?;
|
||||||
|
assert!(
|
||||||
|
tester_a
|
||||||
|
.was_sent_sealed(&sealed_id, tester_b.user_id)
|
||||||
|
.await?,
|
||||||
|
"message was not delivered over the sealed transport"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Turning the feature off locally has to fall back to the named transport
|
||||||
|
// without losing the message.
|
||||||
|
tester_a.set_sealed_sender_enabled(false)?;
|
||||||
|
let named_id = MessageService::new(&tester_a.context)
|
||||||
|
.insert_and_send_text(group_id.clone(), "Named hello".into(), None)
|
||||||
|
.await?;
|
||||||
|
tester_b
|
||||||
|
.wait_for_text_message(&named_id, tester_a.user_id, "Named hello")
|
||||||
|
.await?;
|
||||||
|
tester_a.wait_for_message_ack_by_server(&named_id).await?;
|
||||||
|
assert!(
|
||||||
|
!tester_a
|
||||||
|
.was_sent_sealed(&named_id, tester_b.user_id)
|
||||||
|
.await?,
|
||||||
|
"message was sealed even though the local setting is off"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Withdrawing the announcement has to stop the peer from sealing, too.
|
||||||
|
tester_b
|
||||||
|
.wait_for_contact_sealed_sender(tester_a.user_id, false)
|
||||||
|
.await?;
|
||||||
|
tester_a.set_sealed_sender_enabled(true)?;
|
||||||
|
let back_id = MessageService::new(&tester_b.context)
|
||||||
|
.insert_and_send_text(group_id.clone(), "Back to you".into(), None)
|
||||||
|
.await?;
|
||||||
|
tester_a
|
||||||
|
.wait_for_text_message(&back_id, tester_b.user_id, "Back to you")
|
||||||
|
.await?;
|
||||||
|
assert!(
|
||||||
|
!tester_b.was_sent_sealed(&back_id, tester_a.user_id).await?,
|
||||||
|
"peer sealed a message to a contact that withdrew its announcement"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -412,13 +412,20 @@ impl Tester {
|
||||||
std::fs::create_dir_all(data_dir.join("keyvalue"))?;
|
std::fs::create_dir_all(data_dir.join("keyvalue"))?;
|
||||||
|
|
||||||
let config = rust_lib_twonly::user_config::UserConfig {
|
let config = rust_lib_twonly::user_config::UserConfig {
|
||||||
app_version: 100,
|
// Must be at least the server's `sealed_sender_min_app_version`, or
|
||||||
|
// the server refuses to deliver sealed-sender payloads to a tester.
|
||||||
|
app_version: 119,
|
||||||
device_id: 1,
|
device_id: 1,
|
||||||
can_use_login_token_for_auth: true,
|
can_use_login_token_for_auth: true,
|
||||||
is_user_discovery_enabled: true,
|
is_user_discovery_enabled: true,
|
||||||
user_discovery_threshold: 3,
|
user_discovery_threshold: 3,
|
||||||
user_discovery_share_promotion: true,
|
user_discovery_share_promotion: true,
|
||||||
typing_indicators: true,
|
typing_indicators: true,
|
||||||
|
// Off here, on in the sealed-sender test. Minting a Privacy Pass
|
||||||
|
// batch is a run of P-384 scalar multiplications, and paying that
|
||||||
|
// in every unrelated test would slow the debug-built suite down
|
||||||
|
// for coverage the sealed-sender test already provides.
|
||||||
|
sealed_sender_enabled: false,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
|
|
@ -455,6 +462,81 @@ impl Tester {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_sealed_sender_enabled(&self, enabled: bool) -> anyhow::Result<()> {
|
||||||
|
let path = self
|
||||||
|
._temp_dir
|
||||||
|
.path()
|
||||||
|
.join("data")
|
||||||
|
.join("keyvalue")
|
||||||
|
.join("user.json");
|
||||||
|
let content = std::fs::read_to_string(&path)?;
|
||||||
|
let mut config: rust_lib_twonly::user_config::UserConfig = serde_json::from_str(&content)?;
|
||||||
|
config.sealed_sender_enabled = enabled;
|
||||||
|
std::fs::write(&path, serde_json::to_string(&config)?)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Waits until the contact has announced (or withdrawn) sealed-sender
|
||||||
|
/// support. The announcement rides along with any encrypted content, so it
|
||||||
|
/// only lands once the peer has actually sent something.
|
||||||
|
pub async fn wait_for_contact_sealed_sender(
|
||||||
|
&self,
|
||||||
|
user_id: i64,
|
||||||
|
expected: bool,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
for _ in 0..100 {
|
||||||
|
let database = self.context.app_db.read().await.clone();
|
||||||
|
let enabled = sqlx::query_scalar!(
|
||||||
|
"SELECT sealed_sender_enabled FROM contacts WHERE user_id = ?",
|
||||||
|
user_id
|
||||||
|
)
|
||||||
|
.fetch_optional(&database.pool)
|
||||||
|
.await?;
|
||||||
|
if enabled == Some(i64::from(expected)) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
}
|
||||||
|
Err(anyhow::anyhow!(
|
||||||
|
"contact {user_id} did not reach sealed_sender_enabled = {expected}"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether this message's copy for `contact_id` left as a sealed envelope.
|
||||||
|
pub async fn was_sent_sealed(&self, message_id: &str, contact_id: i64) -> anyhow::Result<bool> {
|
||||||
|
let database = self.context.app_db.read().await.clone();
|
||||||
|
Ok(sqlx::query_scalar!(
|
||||||
|
r#"SELECT EXISTS(SELECT 1 FROM message_actions
|
||||||
|
WHERE message_id = ? AND contact_id = ? AND type = 'sealedSenderAt')"#,
|
||||||
|
message_id,
|
||||||
|
contact_id,
|
||||||
|
)
|
||||||
|
.fetch_one(&database.pool)
|
||||||
|
.await?
|
||||||
|
!= 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Waits for the token pool to fill. Minting is deliberately kept off the
|
||||||
|
/// send path, so it lands shortly after authentication rather than during
|
||||||
|
/// it.
|
||||||
|
///
|
||||||
|
/// The wait is generous because blinding and unblinding a batch are P-384
|
||||||
|
/// scalar multiplications, and this suite is built without optimizations,
|
||||||
|
/// where each one costs orders of magnitude more than in a release build.
|
||||||
|
pub async fn wait_for_privacy_pass_tokens(&self) -> anyhow::Result<()> {
|
||||||
|
for _ in 0..600 {
|
||||||
|
let database = self.context.app_db.read().await.clone();
|
||||||
|
let count = sqlx::query_scalar!("SELECT COUNT(*) FROM privacy_pass_tokens")
|
||||||
|
.fetch_one(&database.pool)
|
||||||
|
.await?;
|
||||||
|
if count > 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
}
|
||||||
|
Err(anyhow::anyhow!("the Privacy Pass token pool stayed empty"))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn update_username(&mut self, new_username: String) -> anyhow::Result<()> {
|
pub fn update_username(&mut self, new_username: String) -> anyhow::Result<()> {
|
||||||
self.username = new_username.clone();
|
self.username = new_username.clone();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,17 +41,32 @@ done
|
||||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
ZELLIJ_CONFIG="${ZELLIJ_CONFIG_FILE:-${XDG_CONFIG_HOME:-$HOME/.config}/zellij/config.kdl}"
|
ZELLIJ_CONFIG="${ZELLIJ_CONFIG_FILE:-${XDG_CONFIG_HOME:-$HOME/.config}/zellij/config.kdl}"
|
||||||
|
|
||||||
if zellij list-sessions -s 2>/dev/null | grep -qx "$SESSION"; then
|
# zellij keeps EXITED sessions around to be resurrected, and they still show up
|
||||||
if [ "$FORCE" -eq 1 ]; then
|
# in `list-sessions`. Only a genuinely live session should block a fresh start.
|
||||||
echo "deleting existing session '$SESSION'..."
|
SESSION_STATE="$(
|
||||||
zellij delete-session --force "$SESSION" >/dev/null
|
zellij list-sessions -n 2>/dev/null \
|
||||||
else
|
| awk -v s="$SESSION" '$1 == s { print (index($0, "EXITED") ? "exited" : "live"); found = 1 }
|
||||||
echo "zellij session '$SESSION' already exists. Attach with:" >&2
|
END { if (!found) print "none" }'
|
||||||
echo " zellij attach $SESSION" >&2
|
)"
|
||||||
echo "or re-run with -f to replace it." >&2
|
|
||||||
exit 1
|
case "$SESSION_STATE" in
|
||||||
fi
|
exited)
|
||||||
fi
|
echo "clearing exited session '$SESSION'..."
|
||||||
|
zellij delete-session --force "$SESSION" >/dev/null 2>&1 || true
|
||||||
|
;;
|
||||||
|
live)
|
||||||
|
if [ "$FORCE" -eq 1 ]; then
|
||||||
|
echo "killing live session '$SESSION'..."
|
||||||
|
zellij kill-session "$SESSION" >/dev/null 2>&1 || true
|
||||||
|
zellij delete-session --force "$SESSION" >/dev/null 2>&1 || true
|
||||||
|
else
|
||||||
|
echo "zellij session '$SESSION' is already running. Attach with:" >&2
|
||||||
|
echo " zellij attach $SESSION" >&2
|
||||||
|
echo "or re-run with -f to replace it." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
echo "querying flutter devices..."
|
echo "querying flutter devices..."
|
||||||
DEVICES_JSON="$(flutter devices --machine)"
|
DEVICES_JSON="$(flutter devices --machine)"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue