mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-05-25 02:12:13 +00:00
start with rust backup
This commit is contained in:
parent
e6a468c065
commit
f323bc03eb
35 changed files with 3436 additions and 317 deletions
|
|
@ -9,7 +9,7 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||||
// These functions are ignored because they are not marked as `pub`: `get_twonly_flutter`
|
// These functions are ignored because they are not marked as `pub`: `get_twonly_flutter`
|
||||||
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `TwonlyFlutter`
|
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `TwonlyFlutter`
|
||||||
|
|
||||||
Future<void> initializeTwonlyFlutter({required TwonlyConfig config}) =>
|
Future<void> initializeTwonlyFlutter({required InitConfig config}) =>
|
||||||
RustLib.instance.api.crateBridgeInitializeTwonlyFlutter(config: config);
|
RustLib.instance.api.crateBridgeInitializeTwonlyFlutter(config: config);
|
||||||
|
|
||||||
class AnnouncedUser {
|
class AnnouncedUser {
|
||||||
|
|
@ -36,6 +36,27 @@ class AnnouncedUser {
|
||||||
publicId == other.publicId;
|
publicId == other.publicId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class InitConfig {
|
||||||
|
final String databaseDir;
|
||||||
|
final String dataDir;
|
||||||
|
|
||||||
|
const InitConfig({
|
||||||
|
required this.databaseDir,
|
||||||
|
required this.dataDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => databaseDir.hashCode ^ dataDir.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is InitConfig &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
databaseDir == other.databaseDir &&
|
||||||
|
dataDir == other.dataDir;
|
||||||
|
}
|
||||||
|
|
||||||
class OtherPromotion {
|
class OtherPromotion {
|
||||||
final int promotionId;
|
final int promotionId;
|
||||||
final PlatformInt64 publicId;
|
final PlatformInt64 publicId;
|
||||||
|
|
@ -74,24 +95,3 @@ class OtherPromotion {
|
||||||
announcementShare == other.announcementShare &&
|
announcementShare == other.announcementShare &&
|
||||||
publicKeyVerifiedTimestamp == other.publicKeyVerifiedTimestamp;
|
publicKeyVerifiedTimestamp == other.publicKeyVerifiedTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
class TwonlyConfig {
|
|
||||||
final String databasePath;
|
|
||||||
final String dataDirectory;
|
|
||||||
|
|
||||||
const TwonlyConfig({
|
|
||||||
required this.databasePath,
|
|
||||||
required this.dataDirectory,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => databasePath.hashCode ^ dataDirectory.hashCode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
other is TwonlyConfig &&
|
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
databasePath == other.databasePath &&
|
|
||||||
dataDirectory == other.dataDirectory;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
28
lib/core/context.dart
Normal file
28
lib/core/context.dart
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
// This file is automatically generated, so please do not edit it.
|
||||||
|
// @generated by `flutter_rust_bridge`@ 2.12.0.
|
||||||
|
|
||||||
|
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
|
||||||
|
|
||||||
|
import 'frb_generated.dart';
|
||||||
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||||
|
|
||||||
|
class InitConfig {
|
||||||
|
final String databasePath;
|
||||||
|
final String dataDirectory;
|
||||||
|
|
||||||
|
const InitConfig({
|
||||||
|
required this.databasePath,
|
||||||
|
required this.dataDirectory,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => databasePath.hashCode ^ dataDirectory.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is InitConfig &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
databasePath == other.databasePath &&
|
||||||
|
dataDirectory == other.dataDirectory;
|
||||||
|
}
|
||||||
|
|
@ -152,9 +152,7 @@ abstract class RustLibApi extends BaseApi {
|
||||||
userDiscoveryGetContactPromotion,
|
userDiscoveryGetContactPromotion,
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<void> crateBridgeInitializeTwonlyFlutter({
|
Future<void> crateBridgeInitializeTwonlyFlutter({required InitConfig config});
|
||||||
required TwonlyConfig config,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
|
|
@ -556,13 +554,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> crateBridgeInitializeTwonlyFlutter({
|
Future<void> crateBridgeInitializeTwonlyFlutter({
|
||||||
required TwonlyConfig config,
|
required InitConfig config,
|
||||||
}) {
|
}) {
|
||||||
return handler.executeNormal(
|
return handler.executeNormal(
|
||||||
NormalTask(
|
NormalTask(
|
||||||
callFfi: (port_) {
|
callFfi: (port_) {
|
||||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
sse_encode_box_autoadd_twonly_config(config, serializer);
|
sse_encode_box_autoadd_init_config(config, serializer);
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
|
|
@ -1180,9 +1178,9 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
TwonlyConfig dco_decode_box_autoadd_twonly_config(dynamic raw) {
|
InitConfig dco_decode_box_autoadd_init_config(dynamic raw) {
|
||||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
return dco_decode_twonly_config(raw);
|
return dco_decode_init_config(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
|
|
@ -1200,6 +1198,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
return dcoDecodeI64(raw);
|
return dcoDecodeI64(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
InitConfig dco_decode_init_config(dynamic raw) {
|
||||||
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
|
final arr = raw as List<dynamic>;
|
||||||
|
if (arr.length != 2)
|
||||||
|
throw Exception('unexpected arr length: expect 2 but see ${arr.length}');
|
||||||
|
return InitConfig(
|
||||||
|
databaseDir: dco_decode_String(arr[0]),
|
||||||
|
dataDir: dco_decode_String(arr[1]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
PlatformInt64 dco_decode_isize(dynamic raw) {
|
PlatformInt64 dco_decode_isize(dynamic raw) {
|
||||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
|
|
@ -1276,18 +1286,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@protected
|
|
||||||
TwonlyConfig dco_decode_twonly_config(dynamic raw) {
|
|
||||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
|
||||||
final arr = raw as List<dynamic>;
|
|
||||||
if (arr.length != 2)
|
|
||||||
throw Exception('unexpected arr length: expect 2 but see ${arr.length}');
|
|
||||||
return TwonlyConfig(
|
|
||||||
databasePath: dco_decode_String(arr[0]),
|
|
||||||
dataDirectory: dco_decode_String(arr[1]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
int dco_decode_u_32(dynamic raw) {
|
int dco_decode_u_32(dynamic raw) {
|
||||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
|
|
@ -1375,11 +1373,9 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
TwonlyConfig sse_decode_box_autoadd_twonly_config(
|
InitConfig sse_decode_box_autoadd_init_config(SseDeserializer deserializer) {
|
||||||
SseDeserializer deserializer,
|
|
||||||
) {
|
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
return (sse_decode_twonly_config(deserializer));
|
return (sse_decode_init_config(deserializer));
|
||||||
}
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
|
|
@ -1396,6 +1392,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
return deserializer.buffer.getPlatformInt64();
|
return deserializer.buffer.getPlatformInt64();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
InitConfig sse_decode_init_config(SseDeserializer deserializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
var var_databaseDir = sse_decode_String(deserializer);
|
||||||
|
var var_dataDir = sse_decode_String(deserializer);
|
||||||
|
return InitConfig(databaseDir: var_databaseDir, dataDir: var_dataDir);
|
||||||
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
PlatformInt64 sse_decode_isize(SseDeserializer deserializer) {
|
PlatformInt64 sse_decode_isize(SseDeserializer deserializer) {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
|
@ -1526,17 +1530,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@protected
|
|
||||||
TwonlyConfig sse_decode_twonly_config(SseDeserializer deserializer) {
|
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
|
||||||
var var_databasePath = sse_decode_String(deserializer);
|
|
||||||
var var_dataDirectory = sse_decode_String(deserializer);
|
|
||||||
return TwonlyConfig(
|
|
||||||
databasePath: var_databasePath,
|
|
||||||
dataDirectory: var_dataDirectory,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
int sse_decode_u_32(SseDeserializer deserializer) {
|
int sse_decode_u_32(SseDeserializer deserializer) {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
|
@ -1820,12 +1813,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_box_autoadd_twonly_config(
|
void sse_encode_box_autoadd_init_config(
|
||||||
TwonlyConfig self,
|
InitConfig self,
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
) {
|
) {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
sse_encode_twonly_config(self, serializer);
|
sse_encode_init_config(self, serializer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
|
|
@ -1842,6 +1835,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
serializer.buffer.putPlatformInt64(self);
|
serializer.buffer.putPlatformInt64(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_init_config(InitConfig self, SseSerializer serializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
sse_encode_String(self.databaseDir, serializer);
|
||||||
|
sse_encode_String(self.dataDir, serializer);
|
||||||
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_isize(PlatformInt64 self, SseSerializer serializer) {
|
void sse_encode_isize(PlatformInt64 self, SseSerializer serializer) {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
|
@ -1976,13 +1976,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@protected
|
|
||||||
void sse_encode_twonly_config(TwonlyConfig self, SseSerializer serializer) {
|
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
|
||||||
sse_encode_String(self.databasePath, serializer);
|
|
||||||
sse_encode_String(self.dataDirectory, serializer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_u_32(int self, SseSerializer serializer) {
|
void sse_encode_u_32(int self, SseSerializer serializer) {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
PlatformInt64 dco_decode_box_autoadd_i_64(dynamic raw);
|
PlatformInt64 dco_decode_box_autoadd_i_64(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
TwonlyConfig dco_decode_box_autoadd_twonly_config(dynamic raw);
|
InitConfig dco_decode_box_autoadd_init_config(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw);
|
FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw);
|
||||||
|
|
@ -125,6 +125,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
PlatformInt64 dco_decode_i_64(dynamic raw);
|
PlatformInt64 dco_decode_i_64(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
InitConfig dco_decode_init_config(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
PlatformInt64 dco_decode_isize(dynamic raw);
|
PlatformInt64 dco_decode_isize(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -158,9 +161,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
OtherPromotion dco_decode_other_promotion(dynamic raw);
|
OtherPromotion dco_decode_other_promotion(dynamic raw);
|
||||||
|
|
||||||
@protected
|
|
||||||
TwonlyConfig dco_decode_twonly_config(dynamic raw);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
int dco_decode_u_32(dynamic raw);
|
int dco_decode_u_32(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -202,9 +202,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
PlatformInt64 sse_decode_box_autoadd_i_64(SseDeserializer deserializer);
|
PlatformInt64 sse_decode_box_autoadd_i_64(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
TwonlyConfig sse_decode_box_autoadd_twonly_config(
|
InitConfig sse_decode_box_autoadd_init_config(SseDeserializer deserializer);
|
||||||
SseDeserializer deserializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
FlutterUserDiscovery sse_decode_flutter_user_discovery(
|
FlutterUserDiscovery sse_decode_flutter_user_discovery(
|
||||||
|
|
@ -214,6 +212,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
PlatformInt64 sse_decode_i_64(SseDeserializer deserializer);
|
PlatformInt64 sse_decode_i_64(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
InitConfig sse_decode_init_config(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
PlatformInt64 sse_decode_isize(SseDeserializer deserializer);
|
PlatformInt64 sse_decode_isize(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
|
@ -257,9 +258,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
OtherPromotion sse_decode_other_promotion(SseDeserializer deserializer);
|
OtherPromotion sse_decode_other_promotion(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
|
||||||
TwonlyConfig sse_decode_twonly_config(SseDeserializer deserializer);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
int sse_decode_u_32(SseDeserializer deserializer);
|
int sse_decode_u_32(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
|
@ -394,8 +392,8 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_box_autoadd_twonly_config(
|
void sse_encode_box_autoadd_init_config(
|
||||||
TwonlyConfig self,
|
InitConfig self,
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -408,6 +406,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer);
|
void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_init_config(InitConfig self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_isize(PlatformInt64 self, SseSerializer serializer);
|
void sse_encode_isize(PlatformInt64 self, SseSerializer serializer);
|
||||||
|
|
||||||
|
|
@ -468,9 +469,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
|
||||||
void sse_encode_twonly_config(TwonlyConfig self, SseSerializer serializer);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_u_32(int self, SseSerializer serializer);
|
void sse_encode_u_32(int self, SseSerializer serializer);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
PlatformInt64 dco_decode_box_autoadd_i_64(dynamic raw);
|
PlatformInt64 dco_decode_box_autoadd_i_64(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
TwonlyConfig dco_decode_box_autoadd_twonly_config(dynamic raw);
|
InitConfig dco_decode_box_autoadd_init_config(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw);
|
FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw);
|
||||||
|
|
@ -127,6 +127,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
PlatformInt64 dco_decode_i_64(dynamic raw);
|
PlatformInt64 dco_decode_i_64(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
InitConfig dco_decode_init_config(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
PlatformInt64 dco_decode_isize(dynamic raw);
|
PlatformInt64 dco_decode_isize(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -160,9 +163,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
OtherPromotion dco_decode_other_promotion(dynamic raw);
|
OtherPromotion dco_decode_other_promotion(dynamic raw);
|
||||||
|
|
||||||
@protected
|
|
||||||
TwonlyConfig dco_decode_twonly_config(dynamic raw);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
int dco_decode_u_32(dynamic raw);
|
int dco_decode_u_32(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -204,9 +204,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
PlatformInt64 sse_decode_box_autoadd_i_64(SseDeserializer deserializer);
|
PlatformInt64 sse_decode_box_autoadd_i_64(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
TwonlyConfig sse_decode_box_autoadd_twonly_config(
|
InitConfig sse_decode_box_autoadd_init_config(SseDeserializer deserializer);
|
||||||
SseDeserializer deserializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
FlutterUserDiscovery sse_decode_flutter_user_discovery(
|
FlutterUserDiscovery sse_decode_flutter_user_discovery(
|
||||||
|
|
@ -216,6 +214,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
PlatformInt64 sse_decode_i_64(SseDeserializer deserializer);
|
PlatformInt64 sse_decode_i_64(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
InitConfig sse_decode_init_config(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
PlatformInt64 sse_decode_isize(SseDeserializer deserializer);
|
PlatformInt64 sse_decode_isize(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
|
@ -259,9 +260,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
OtherPromotion sse_decode_other_promotion(SseDeserializer deserializer);
|
OtherPromotion sse_decode_other_promotion(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
|
||||||
TwonlyConfig sse_decode_twonly_config(SseDeserializer deserializer);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
int sse_decode_u_32(SseDeserializer deserializer);
|
int sse_decode_u_32(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
|
@ -396,8 +394,8 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_box_autoadd_twonly_config(
|
void sse_encode_box_autoadd_init_config(
|
||||||
TwonlyConfig self,
|
InitConfig self,
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -410,6 +408,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer);
|
void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_init_config(InitConfig self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_isize(PlatformInt64 self, SseSerializer serializer);
|
void sse_encode_isize(PlatformInt64 self, SseSerializer serializer);
|
||||||
|
|
||||||
|
|
@ -470,9 +471,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
|
||||||
void sse_encode_twonly_config(TwonlyConfig self, SseSerializer serializer);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_u_32(int self, SseSerializer serializer);
|
void sse_encode_u_32(int self, SseSerializer serializer);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,9 +55,9 @@ Future<void> twonlyMinimumInitialization() async {
|
||||||
|
|
||||||
Log.info('twonlyMinimumInitialization: bridge.initializeTwonlyFlutter()');
|
Log.info('twonlyMinimumInitialization: bridge.initializeTwonlyFlutter()');
|
||||||
await bridge.initializeTwonlyFlutter(
|
await bridge.initializeTwonlyFlutter(
|
||||||
config: bridge.TwonlyConfig(
|
config: bridge.InitConfig(
|
||||||
databasePath: '${AppEnvironment.supportDir}/twonly.sqlite',
|
databaseDir: AppEnvironment.supportDir,
|
||||||
dataDirectory: AppEnvironment.supportDir,
|
dataDir: AppEnvironment.supportDir,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
Log.info('twonlyMinimumInitialization: finished');
|
Log.info('twonlyMinimumInitialization: finished');
|
||||||
|
|
|
||||||
1921
rust/Cargo.lock
generated
1921
rust/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -18,13 +18,37 @@ sqlx = { version = "0.9.0-alpha.1", default-features = false, features = [
|
||||||
"derive",
|
"derive",
|
||||||
"json",
|
"json",
|
||||||
] }
|
] }
|
||||||
|
mdk-core = { version = "0.8.0", git = "https://github.com/marmot-protocol/mdk", rev = "7f809f8549458a0d7f7d885bcdd694023abf299c", features = [
|
||||||
|
"mip04",
|
||||||
|
"mip05",
|
||||||
|
] }
|
||||||
|
mdk-sqlite-storage = { version = "0.8.0", git = "https://github.com/marmot-protocol/mdk", rev = "7f809f8549458a0d7f7d885bcdd694023abf299c" }
|
||||||
|
mdk-storage-traits = { version = "0.8.0", git = "https://github.com/marmot-protocol/mdk", rev = "7f809f8549458a0d7f7d885bcdd694023abf299c" }
|
||||||
|
libsqlite3-sys = { version = "0.35.0", features = ["bundled", "sqlcipher"] }
|
||||||
tokio = { version = "1.44", features = ["full"] }
|
tokio = { version = "1.44", features = ["full"] }
|
||||||
tracing = "0.1.44"
|
tracing = "0.1.44"
|
||||||
rand = "0.10.1"
|
rand = "0.10.1"
|
||||||
protocols = { path = "../rust_dependencies/protocols" }
|
protocols = { path = "../rust_dependencies/protocols" }
|
||||||
|
hkdf = "0.12.4"
|
||||||
|
sha2 = "0.10.8"
|
||||||
|
aes-gcm = "0.10.3"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
tracing-appender = "0.2.5"
|
tracing-appender = "0.2.5"
|
||||||
paste = "1.0.15"
|
paste = "1.0.15"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
zeroize = { version = "1.8", features = ["derive"] }
|
||||||
|
hex = "0.4.3"
|
||||||
|
keyring-core = "1"
|
||||||
|
postcard = { version = "1.0", features = ["alloc"] }
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
zip = { version = "2.2.2", default-features = false, features = ["deflate"] }
|
||||||
|
walkdir = "2.5.0"
|
||||||
|
[target.'cfg(target_os = "ios")'.dependencies]
|
||||||
|
# iOS backend: Requires the 'protected' feature for Data Protection Keychain
|
||||||
|
apple-native-keyring-store = { version = "1", features = ["protected"] }
|
||||||
|
[target.'cfg(target_os = "android")'.dependencies]
|
||||||
|
# Android backend: Interfaces with the Android Keystore
|
||||||
|
android-native-keyring-store = "1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_env_logger = "0.5.0"
|
pretty_env_logger = "0.5.0"
|
||||||
|
|
|
||||||
258
rust/src/backup/backup_archive.rs
Normal file
258
rust/src/backup/backup_archive.rs
Normal file
|
|
@ -0,0 +1,258 @@
|
||||||
|
use crate::context::Context;
|
||||||
|
use crate::database::Database;
|
||||||
|
use crate::error::Result;
|
||||||
|
use crate::keys::DatabaseKey;
|
||||||
|
use std::fs::{remove_file, File};
|
||||||
|
use std::io::{copy, Cursor};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use walkdir::WalkDir;
|
||||||
|
use zeroize::Zeroize;
|
||||||
|
use zip::write::SimpleFileOptions;
|
||||||
|
use zip::{CompressionMethod, ZipArchive, ZipWriter};
|
||||||
|
|
||||||
|
struct BackupArchive {}
|
||||||
|
|
||||||
|
impl BackupArchive {
|
||||||
|
fn get_backup_files(ctx: &Context) -> Result<Vec<(&str, PathBuf, bool, Option<String>)>> {
|
||||||
|
let config = ctx.get_config()?;
|
||||||
|
let database_dir = PathBuf::from(&config.database_dir);
|
||||||
|
let data_dir = PathBuf::from(&config.data_dir);
|
||||||
|
let keys = ctx.get_key_manager()?;
|
||||||
|
let rust_db_key = keys.main_key.get_database_key(DatabaseKey::RustDb);
|
||||||
|
|
||||||
|
Ok(vec![
|
||||||
|
("twonly.sqlite", database_dir.clone(), true, None),
|
||||||
|
("rust_db.sqlite", database_dir, true, Some(rust_db_key)),
|
||||||
|
("user_discovery_config.json", data_dir, false, None),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn create_backup(ctx: &Context) -> Result<PathBuf> {
|
||||||
|
let config = ctx.get_config()?;
|
||||||
|
let data_dir = PathBuf::from(&config.data_dir);
|
||||||
|
|
||||||
|
let backup_data_dir = data_dir.join("temp_backup_dir");
|
||||||
|
if backup_data_dir.is_dir() {
|
||||||
|
std::fs::remove_dir_all(&backup_data_dir)?;
|
||||||
|
}
|
||||||
|
std::fs::create_dir_all(&backup_data_dir)?;
|
||||||
|
|
||||||
|
for (file_name, source_dir, is_db, mut encryption_key) in Self::get_backup_files(ctx)? {
|
||||||
|
let file_path = source_dir.join(&file_name);
|
||||||
|
if !file_path.exists() {
|
||||||
|
tracing::warn!(
|
||||||
|
"Could not backup {} as it does not exist.",
|
||||||
|
file_path.display()
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_db {
|
||||||
|
let db = Database::new(
|
||||||
|
&file_path.display().to_string(),
|
||||||
|
encryption_key.as_deref(),
|
||||||
|
encryption_key.is_none(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let backup_database_file = backup_data_dir.join(&file_name).display().to_string();
|
||||||
|
db.create_backup(backup_database_file.as_str(), encryption_key.as_deref())
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
let file_backup = backup_data_dir.join(&file_name);
|
||||||
|
std::fs::copy(file_path, file_backup)?;
|
||||||
|
}
|
||||||
|
encryption_key.zeroize();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut zip_data = Vec::new();
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut zip = ZipWriter::new(Cursor::new(&mut zip_data));
|
||||||
|
let options =
|
||||||
|
SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
|
||||||
|
|
||||||
|
for entry in WalkDir::new(&backup_data_dir) {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
if !path.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(name) = path.strip_prefix(&backup_data_dir) {
|
||||||
|
zip.start_file(name.to_string_lossy(), options)?;
|
||||||
|
copy(&mut File::open(path)?, &mut zip)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
zip.finish()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut keys = ctx.get_key_manager()?;
|
||||||
|
|
||||||
|
let zip_path = data_dir.join("temp_backup.zip");
|
||||||
|
std::fs::write(&zip_path, keys.main_key.encrypt_backup(&zip_data))?;
|
||||||
|
|
||||||
|
std::fs::remove_dir_all(&backup_data_dir)?;
|
||||||
|
keys.zeroize();
|
||||||
|
|
||||||
|
Ok(zip_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn restore_from_backup(ctx: &Context, file_path: &PathBuf) -> Result<()> {
|
||||||
|
let data_dir = PathBuf::from(&ctx.get_config()?.data_dir);
|
||||||
|
|
||||||
|
let mut keys = ctx.get_key_manager()?;
|
||||||
|
|
||||||
|
let encrypted_zip = std::fs::read(file_path)?;
|
||||||
|
let zip_content = keys.main_key.decrypt_backup(&encrypted_zip)?;
|
||||||
|
|
||||||
|
let restore_temp_dir = data_dir.join("restore_temp");
|
||||||
|
|
||||||
|
if restore_temp_dir.exists() {
|
||||||
|
std::fs::remove_dir_all(&restore_temp_dir)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fs::create_dir_all(&restore_temp_dir)?;
|
||||||
|
|
||||||
|
let mut archive = ZipArchive::new(Cursor::new(zip_content))?;
|
||||||
|
|
||||||
|
for i in 0..archive.len() {
|
||||||
|
let mut file = archive.by_index(i)?;
|
||||||
|
|
||||||
|
if file.is_file() {
|
||||||
|
let enclosed_name = file.enclosed_name();
|
||||||
|
if let Some(name) = enclosed_name.as_ref().and_then(|p| p.file_name()) {
|
||||||
|
let restored_file = restore_temp_dir.join(name);
|
||||||
|
copy(&mut file, &mut File::create(&restored_file)?)?;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (file_name, target_dir, is_db, _) in Self::get_backup_files(ctx)? {
|
||||||
|
let src = restore_temp_dir.join(&file_name);
|
||||||
|
if src.exists() {
|
||||||
|
let dst = target_dir.join(&file_name);
|
||||||
|
if is_db {
|
||||||
|
// Remove existing database and its temporary files (WAL, SHM)
|
||||||
|
let _ = remove_file(&dst);
|
||||||
|
let _ = remove_file(target_dir.join(format!("{}-wal", file_name)));
|
||||||
|
let _ = remove_file(target_dir.join(format!("{}-shm", file_name)));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fs::copy(src, dst)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keys.zeroize();
|
||||||
|
std::fs::remove_dir_all(&restore_temp_dir)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_backup_and_restore() {
|
||||||
|
let _ = pretty_env_logger::try_init();
|
||||||
|
|
||||||
|
let temp_dir = tempdir().unwrap();
|
||||||
|
|
||||||
|
let ctx = Context::init_for_testing(
|
||||||
|
temp_dir.path().join("database"),
|
||||||
|
temp_dir.path().join("data"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// 1. Add some data
|
||||||
|
{
|
||||||
|
let config = ctx.get_config().unwrap();
|
||||||
|
let rust_db_path = PathBuf::from(&config.database_dir).join("rust_db.sqlite");
|
||||||
|
let key_manager = ctx.get_key_manager().unwrap();
|
||||||
|
let db = Database::new(
|
||||||
|
&rust_db_path.display().to_string(),
|
||||||
|
Some(&key_manager.main_key.get_database_key(DatabaseKey::RustDb)),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
crate::database::tables::received_messages::ReceivedMessage::insert(
|
||||||
|
&db.pool,
|
||||||
|
"sender1",
|
||||||
|
b"original message",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Add a file
|
||||||
|
let config_file = PathBuf::from(&config.data_dir).join("user_discovery_config.json");
|
||||||
|
std::fs::write(config_file, "original config").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Create backup
|
||||||
|
let backup_path = BackupArchive::create_backup(&ctx).await.unwrap();
|
||||||
|
assert!(backup_path.exists());
|
||||||
|
|
||||||
|
// 3. Modify data (to simulate state before restore)
|
||||||
|
{
|
||||||
|
let config = ctx.get_config().unwrap();
|
||||||
|
let rust_db_path = PathBuf::from(&config.database_dir).join("rust_db.sqlite");
|
||||||
|
let key_manager = ctx.get_key_manager().unwrap();
|
||||||
|
let db = Database::new(
|
||||||
|
&rust_db_path.display().to_string(),
|
||||||
|
Some(&key_manager.main_key.get_database_key(DatabaseKey::RustDb)),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
crate::database::tables::received_messages::ReceivedMessage::insert(
|
||||||
|
&db.pool,
|
||||||
|
"sender2",
|
||||||
|
b"new message",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let config_file = PathBuf::from(&config.data_dir).join("user_discovery_config.json");
|
||||||
|
std::fs::write(config_file, "new config").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Restore backup
|
||||||
|
BackupArchive::restore_from_backup(&ctx, &backup_path)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// 5. Verify restored data
|
||||||
|
{
|
||||||
|
let config = ctx.get_config().unwrap();
|
||||||
|
let rust_db_path = PathBuf::from(&config.database_dir).join("rust_db.sqlite");
|
||||||
|
let key_manager = ctx.get_key_manager().unwrap();
|
||||||
|
let db = Database::new(
|
||||||
|
&rust_db_path.display().to_string(),
|
||||||
|
Some(&key_manager.main_key.get_database_key(DatabaseKey::RustDb)),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let messages =
|
||||||
|
crate::database::tables::received_messages::ReceivedMessage::get_all(&db.pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
// Should only have the original message because restore overwrites
|
||||||
|
assert_eq!(messages.len(), 1);
|
||||||
|
assert_eq!(messages[0].sender_id, "sender1");
|
||||||
|
assert_eq!(messages[0].content, b"original message");
|
||||||
|
|
||||||
|
let config_file = PathBuf::from(&config.data_dir).join("user_discovery_config.json");
|
||||||
|
let config_content = std::fs::read_to_string(config_file).unwrap();
|
||||||
|
assert_eq!(config_content, "original config");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
rust/src/backup/mod.rs
Normal file
1
rust/src/backup/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
mod backup_archive;
|
||||||
|
|
@ -5,12 +5,10 @@ pub(crate) mod user_discovery;
|
||||||
use flutter_rust_bridge::DartFnFuture;
|
use flutter_rust_bridge::DartFnFuture;
|
||||||
use protocols::user_discovery::traits::{AnnouncedUser, OtherPromotion};
|
use protocols::user_discovery::traits::{AnnouncedUser, OtherPromotion};
|
||||||
|
|
||||||
use super::error::Result;
|
use crate::error::{Result, TwonlyError};
|
||||||
use crate::{callback_generator, frb_generated::StreamSink};
|
use crate::{callback_generator, frb_generated::StreamSink};
|
||||||
use std::sync::{Arc, OnceLock};
|
use std::sync::{Arc, OnceLock};
|
||||||
|
|
||||||
use crate::bridge::error::TwonlyError;
|
|
||||||
|
|
||||||
static FLUTTER_CALLBACKS: OnceLock<FlutterCallbacks> = OnceLock::new();
|
static FLUTTER_CALLBACKS: OnceLock<FlutterCallbacks> = OnceLock::new();
|
||||||
|
|
||||||
// This will also generate the function init_flutter_callbacks which MUST be called from Flutter to initialize the callbacks
|
// This will also generate the function init_flutter_callbacks which MUST be called from Flutter to initialize the callbacks
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::bridge::callbacks::get_callbacks;
|
use crate::bridge::callbacks::get_callbacks;
|
||||||
use crate::bridge::error::TwonlyError;
|
|
||||||
use crate::bridge::get_twonly_flutter;
|
use crate::bridge::get_twonly_flutter;
|
||||||
|
use crate::error::TwonlyError;
|
||||||
use protocols::user_discovery::error::{Result, UserDiscoveryError};
|
use protocols::user_discovery::error::{Result, UserDiscoveryError};
|
||||||
use protocols::user_discovery::traits::UserDiscoveryUtils;
|
use protocols::user_discovery::traits::UserDiscoveryUtils;
|
||||||
use protocols::user_discovery::traits::{AnnouncedUser, OtherPromotion, UserDiscoveryStore};
|
use protocols::user_discovery::traits::{AnnouncedUser, OtherPromotion, UserDiscoveryStore};
|
||||||
|
|
@ -47,8 +47,7 @@ impl UserDiscoveryUtils for UserDiscoveryUtilsFlutter {
|
||||||
impl UserDiscoveryStore for UserDiscoveryStoreFlutter {
|
impl UserDiscoveryStore for UserDiscoveryStoreFlutter {
|
||||||
async fn get_config(&self) -> Result<String> {
|
async fn get_config(&self) -> Result<String> {
|
||||||
let ws = get_twonly_flutter()?;
|
let ws = get_twonly_flutter()?;
|
||||||
let config_path =
|
let config_path = PathBuf::from(&ws.config.data_dir).join("user_discovery_config.json");
|
||||||
PathBuf::from(&ws.config.data_directory).join("user_discovery_config.json");
|
|
||||||
|
|
||||||
if !config_path.is_file() {
|
if !config_path.is_file() {
|
||||||
return Err(UserDiscoveryError::NotInitialized);
|
return Err(UserDiscoveryError::NotInitialized);
|
||||||
|
|
@ -60,8 +59,7 @@ impl UserDiscoveryStore for UserDiscoveryStoreFlutter {
|
||||||
async fn update_config(&self, update: String) -> Result<()> {
|
async fn update_config(&self, update: String) -> Result<()> {
|
||||||
tracing::debug!("Updating configuration file.");
|
tracing::debug!("Updating configuration file.");
|
||||||
let ws = get_twonly_flutter()?;
|
let ws = get_twonly_flutter()?;
|
||||||
let config_path =
|
let config_path = PathBuf::from(&ws.config.data_dir).join("user_discovery_config.json");
|
||||||
PathBuf::from(&ws.config.data_directory).join("user_discovery_config.json");
|
|
||||||
std::fs::write(config_path, &update)?;
|
std::fs::write(config_path, &update)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,30 @@
|
||||||
#![allow(unexpected_cfgs)]
|
#![allow(unexpected_cfgs)]
|
||||||
pub mod callbacks;
|
pub mod callbacks;
|
||||||
pub mod error;
|
|
||||||
pub mod log;
|
|
||||||
pub mod wrapper;
|
pub mod wrapper;
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::bridge::callbacks::user_discovery::{
|
use crate::bridge::callbacks::user_discovery::{
|
||||||
UserDiscoveryStoreFlutter, UserDiscoveryUtilsFlutter,
|
UserDiscoveryStoreFlutter, UserDiscoveryUtilsFlutter,
|
||||||
};
|
};
|
||||||
use crate::bridge::log::init_tracing;
|
use crate::context::Context;
|
||||||
|
use crate::database::Database;
|
||||||
|
use crate::error::Result;
|
||||||
|
use crate::error::TwonlyError;
|
||||||
|
use crate::secure_storage::SecureStorage;
|
||||||
use crate::utils::Shared;
|
use crate::utils::Shared;
|
||||||
use error::Result;
|
|
||||||
use error::TwonlyError;
|
|
||||||
use flutter_rust_bridge::frb;
|
use flutter_rust_bridge::frb;
|
||||||
use protocols::user_discovery::UserDiscovery;
|
use protocols::user_discovery::UserDiscovery;
|
||||||
use std::path::PathBuf;
|
|
||||||
use tokio::sync::OnceCell;
|
|
||||||
|
|
||||||
pub use protocols::user_discovery::traits::AnnouncedUser;
|
pub use protocols::user_discovery::traits::AnnouncedUser;
|
||||||
pub use protocols::user_discovery::traits::OtherPromotion;
|
pub use protocols::user_discovery::traits::OtherPromotion;
|
||||||
|
|
||||||
|
pub struct InitConfig {
|
||||||
|
pub database_dir: String,
|
||||||
|
pub data_dir: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[frb(mirror(OtherPromotion))]
|
#[frb(mirror(OtherPromotion))]
|
||||||
pub struct _OtherPromotion {
|
pub struct _OtherPromotion {
|
||||||
pub promotion_id: u32,
|
pub promotion_id: u32,
|
||||||
|
|
@ -36,58 +42,25 @@ pub struct _AnnouncedUser {
|
||||||
pub public_id: i64,
|
pub public_id: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TwonlyConfig {
|
|
||||||
pub database_path: String,
|
|
||||||
pub data_directory: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct TwonlyFlutter {
|
pub(crate) struct TwonlyFlutter {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub(crate) config: TwonlyConfig,
|
pub(crate) config: InitConfig,
|
||||||
// /// Rust runs in the same process as drift, the database can only be opened in readonly mode
|
|
||||||
// pub(crate) twonly_db_readonly: Arc<Database>,
|
|
||||||
pub(crate) user_discovery:
|
pub(crate) user_discovery:
|
||||||
Shared<UserDiscovery<UserDiscoveryStoreFlutter, UserDiscoveryUtilsFlutter>>,
|
Shared<UserDiscovery<UserDiscoveryStoreFlutter, UserDiscoveryUtilsFlutter>>,
|
||||||
|
pub(crate) rust_db: Arc<Database>,
|
||||||
|
pub(crate) secure_storage: SecureStorage,
|
||||||
}
|
}
|
||||||
|
|
||||||
static GLOBAL_TWONLY: OnceCell<TwonlyFlutter> = OnceCell::const_new();
|
|
||||||
|
|
||||||
pub(super) fn get_twonly_flutter() -> Result<&'static TwonlyFlutter> {
|
pub(super) fn get_twonly_flutter() -> Result<&'static TwonlyFlutter> {
|
||||||
GLOBAL_TWONLY.get().ok_or(TwonlyError::Initialization)
|
let ctx = Context::get_static()?;
|
||||||
|
if let Context::Flutter(twonly) = ctx {
|
||||||
|
return Ok(twonly);
|
||||||
|
} else {
|
||||||
|
return Err(TwonlyError::Initialization);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn initialize_twonly_flutter(config: TwonlyConfig) -> Result<()> {
|
pub async fn initialize_twonly_flutter(config: InitConfig) -> Result<()> {
|
||||||
if GLOBAL_TWONLY.initialized() {
|
Context::init_flutter(config).await?;
|
||||||
tracing::info!("twonly already initialized.");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let log_dir = PathBuf::from(&config.data_directory).join("log");
|
|
||||||
init_tracing(&log_dir, true).await;
|
|
||||||
tracing::info!("Initialized twonly workspace.");
|
|
||||||
let twonly_res: Result<&'static TwonlyFlutter> = GLOBAL_TWONLY
|
|
||||||
.get_or_try_init(|| async {
|
|
||||||
// let database_dir = PathBuf::from(&config.database_path.clone());
|
|
||||||
// let Some(rust_db_path) = database_dir.parent() else {
|
|
||||||
// return Err(TwonlyError::DatabaseNotFound);
|
|
||||||
// };
|
|
||||||
// let rust_db_path = rust_db_path.join("rust_db.sqlite").display().to_string();
|
|
||||||
|
|
||||||
// let twonly_db_readonly = Arc::new(Database::new(&config.database_path, true).await?);
|
|
||||||
// let rust_db = Arc::new(Database::new(&rust_db_path, false).await?);
|
|
||||||
|
|
||||||
Ok(TwonlyFlutter {
|
|
||||||
config,
|
|
||||||
// twonly_db_readonly,
|
|
||||||
// rust_db,
|
|
||||||
user_discovery: Shared::new(UserDiscovery::new(
|
|
||||||
UserDiscoveryStoreFlutter {},
|
|
||||||
UserDiscoveryUtilsFlutter {},
|
|
||||||
)?),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
twonly_res?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::bridge::error::Result;
|
|
||||||
use crate::bridge::get_twonly_flutter;
|
use crate::bridge::get_twonly_flutter;
|
||||||
|
use crate::error::Result;
|
||||||
|
|
||||||
pub struct FlutterUserDiscovery {}
|
pub struct FlutterUserDiscovery {}
|
||||||
|
|
||||||
|
|
|
||||||
169
rust/src/context.rs
Normal file
169
rust/src/context.rs
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
use crate::{
|
||||||
|
bridge::{
|
||||||
|
callbacks::user_discovery::{UserDiscoveryStoreFlutter, UserDiscoveryUtilsFlutter},
|
||||||
|
InitConfig,
|
||||||
|
},
|
||||||
|
database::Database,
|
||||||
|
error::{Result, TwonlyError},
|
||||||
|
keys::{DatabaseKey, KeyManager},
|
||||||
|
log::init_tracing,
|
||||||
|
utils::Shared,
|
||||||
|
};
|
||||||
|
use protocols::user_discovery::UserDiscovery;
|
||||||
|
use std::{path::PathBuf, sync::Arc};
|
||||||
|
use tokio::sync::OnceCell;
|
||||||
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
|
use crate::{bridge::TwonlyFlutter, secure_storage::SecureStorage, standalone::TwonlyStandalone};
|
||||||
|
|
||||||
|
pub(crate) enum Context {
|
||||||
|
Flutter(TwonlyFlutter),
|
||||||
|
Standalone(TwonlyStandalone),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Context {
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn from_standalone(standalone: TwonlyStandalone) -> Self {
|
||||||
|
Self::Standalone(standalone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static GLOBAL_CONTEXT: OnceCell<Context> = OnceCell::const_new();
|
||||||
|
|
||||||
|
impl Context {
|
||||||
|
pub(crate) async fn init_flutter(config: InitConfig) -> Result<()> {
|
||||||
|
Self::init_common(config, true).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn init_standalone(config: InitConfig) -> Result<()> {
|
||||||
|
Self::init_common(config, false).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) async fn init_for_testing(
|
||||||
|
database_dir: PathBuf,
|
||||||
|
data_dir: PathBuf,
|
||||||
|
) -> Result<Context> {
|
||||||
|
std::fs::create_dir_all(&database_dir)?;
|
||||||
|
std::fs::create_dir_all(&data_dir)?;
|
||||||
|
|
||||||
|
let config = InitConfig {
|
||||||
|
database_dir: database_dir.display().to_string(),
|
||||||
|
data_dir: data_dir.display().to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize tracing and secure storage if not already done
|
||||||
|
let _ = SecureStorage::init();
|
||||||
|
let secure_storage = SecureStorage::new("eu.twonly.testing");
|
||||||
|
|
||||||
|
let key_manager = KeyManager::generate()?;
|
||||||
|
key_manager.store_to_keychain(&secure_storage)?;
|
||||||
|
|
||||||
|
let rust_db_path = database_dir.join("rust_db.sqlite");
|
||||||
|
let rust_db = Arc::new(
|
||||||
|
Database::new(
|
||||||
|
&rust_db_path.display().to_string(),
|
||||||
|
Some(&key_manager.main_key.get_database_key(DatabaseKey::RustDb)),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(Context::from_standalone(TwonlyStandalone {
|
||||||
|
config,
|
||||||
|
rust_db,
|
||||||
|
secure_storage,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn init_common(config: InitConfig, is_flutter: bool) -> Result<()> {
|
||||||
|
if GLOBAL_CONTEXT.initialized() {
|
||||||
|
tracing::info!("twonly already initialized.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let log_dir = PathBuf::from(&config.data_dir).join("log");
|
||||||
|
init_tracing(&log_dir, is_flutter).await;
|
||||||
|
|
||||||
|
SecureStorage::init()?;
|
||||||
|
let secure_storage = SecureStorage::new("eu.twonly");
|
||||||
|
|
||||||
|
let database_dir = PathBuf::from(&config.database_dir.clone());
|
||||||
|
let rust_db_path = database_dir.join("rust_db.sqlite");
|
||||||
|
|
||||||
|
tracing::info!("Initialized twonly workspace.");
|
||||||
|
let _: Result<&'static Context> = GLOBAL_CONTEXT
|
||||||
|
.get_or_try_init(|| async {
|
||||||
|
let key_manager = match KeyManager::try_from_keychain(&secure_storage) {
|
||||||
|
Ok(key) => key,
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!("{err}");
|
||||||
|
if rust_db_path.exists() {
|
||||||
|
tracing::error!("Rust Database exsist, while the key manager not");
|
||||||
|
return Err(TwonlyError::SecureStorageError);
|
||||||
|
}
|
||||||
|
tracing::info!("Generating a new key manager.");
|
||||||
|
let new = KeyManager::generate()?;
|
||||||
|
new.store_to_keychain(&secure_storage)?;
|
||||||
|
new
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut rust_db_key = key_manager.main_key.get_database_key(DatabaseKey::RustDb);
|
||||||
|
|
||||||
|
let rust_db = Arc::new(
|
||||||
|
Database::new(
|
||||||
|
&rust_db_path.display().to_string(),
|
||||||
|
Some(rust_db_key.as_str()),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
);
|
||||||
|
|
||||||
|
rust_db_key.zeroize();
|
||||||
|
|
||||||
|
if is_flutter {
|
||||||
|
Ok(Context::Flutter(TwonlyFlutter {
|
||||||
|
config,
|
||||||
|
secure_storage,
|
||||||
|
rust_db,
|
||||||
|
user_discovery: Shared::new(UserDiscovery::new(
|
||||||
|
UserDiscoveryStoreFlutter {},
|
||||||
|
UserDiscoveryUtilsFlutter {},
|
||||||
|
)?),
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
Ok(Context::Standalone(TwonlyStandalone {
|
||||||
|
config,
|
||||||
|
rust_db,
|
||||||
|
secure_storage,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn get_static() -> Result<&'static Context> {
|
||||||
|
GLOBAL_CONTEXT.get().ok_or(TwonlyError::Initialization)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_secure_storage(&self) -> Result<&SecureStorage> {
|
||||||
|
match self {
|
||||||
|
Self::Flutter(twonly) => Ok(&twonly.secure_storage),
|
||||||
|
Self::Standalone(twonly) => Ok(&twonly.secure_storage),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_config(&self) -> Result<&InitConfig> {
|
||||||
|
match self {
|
||||||
|
Self::Flutter(twonly) => Ok(&twonly.config),
|
||||||
|
Self::Standalone(twonly) => Ok(&twonly.config),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_key_manager(&self) -> Result<KeyManager> {
|
||||||
|
KeyManager::try_from_keychain(self.get_secure_storage()?)
|
||||||
|
}
|
||||||
|
}
|
||||||
16
rust/src/database/migrations/0001_initial.sql
Normal file
16
rust/src/database/migrations/0001_initial.sql
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
-- Initial migration: Create received_messages and sending_messages tables
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS received_messages (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
sender_id TEXT NOT NULL,
|
||||||
|
content BLOB NOT NULL,
|
||||||
|
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS sending_messages (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
recipient_id TEXT NOT NULL,
|
||||||
|
content BLOB NOT NULL,
|
||||||
|
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
status TEXT DEFAULT 'pending'
|
||||||
|
);
|
||||||
|
|
@ -1,65 +1,180 @@
|
||||||
// use crate::bridge::error::{Result, TwonlyError};
|
use crate::error::{Result, TwonlyError};
|
||||||
// use sqlx::migrate::MigrateDatabase;
|
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
|
||||||
// use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
|
use sqlx::{ConnectOptions, SqlitePool};
|
||||||
// use sqlx::{ConnectOptions, Sqlite, SqlitePool};
|
use std::time::Duration;
|
||||||
// use std::time::Duration;
|
|
||||||
|
|
||||||
// pub(crate) struct Database {
|
pub(crate) mod tables;
|
||||||
// pub(crate) pool: SqlitePool,
|
|
||||||
// }
|
|
||||||
|
|
||||||
// impl Database {
|
pub(crate) struct Database {
|
||||||
// pub(crate) async fn new(db_path: &String, read_only: bool) -> Result<Self> {
|
pub(crate) pool: SqlitePool,
|
||||||
// let db_url = format!("sqlite://{}", db_path);
|
}
|
||||||
|
|
||||||
// match Sqlite::database_exists(&db_url).await {
|
impl Database {
|
||||||
// Ok(true) => {
|
pub(crate) async fn new(
|
||||||
// tracing::debug!("database exists");
|
db_path: &String,
|
||||||
// }
|
encryption_key: Option<&str>,
|
||||||
// Ok(false) => {
|
read_only: bool,
|
||||||
// tracing::error!("could not open the sqlite3 database");
|
) -> Result<Self> {
|
||||||
// return Err(TwonlyError::DatabaseNotFound);
|
let db_url = format!("sqlite://{}", db_path);
|
||||||
// }
|
|
||||||
// Err(e) => {
|
|
||||||
// tracing::error!(
|
|
||||||
// "Could not check if database exists: {:?}, attempting to create",
|
|
||||||
// e
|
|
||||||
// );
|
|
||||||
// return Err(TwonlyError::DatabaseNotFound);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// tracing::debug!("Creating database connection pool");
|
let log_statements_level = if std::env::var("SQLX_LOG_STATEMENTS").is_ok() {
|
||||||
|
tracing::log::LevelFilter::Info
|
||||||
|
} else {
|
||||||
|
tracing::log::LevelFilter::Off
|
||||||
|
};
|
||||||
|
|
||||||
// let log_statements_level = if std::env::var("SQLX_LOG_STATEMENTS").is_ok() {
|
let mut connect_options = format!("{db_url}?mode=rwc")
|
||||||
// tracing::log::LevelFilter::Info
|
.parse::<SqliteConnectOptions>()?
|
||||||
// } else {
|
.log_statements(log_statements_level)
|
||||||
// tracing::log::LevelFilter::Off
|
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
|
||||||
// };
|
.foreign_keys(true)
|
||||||
|
.read_only(read_only)
|
||||||
|
.busy_timeout(Duration::from_millis(5000))
|
||||||
|
.pragma("recursive_triggers", "ON")
|
||||||
|
.log_slow_statements(tracing::log::LevelFilter::Warn, Duration::from_millis(500));
|
||||||
|
|
||||||
// let connect_options = format!("{db_url}?mode=rwc")
|
if let Some(encryption_key) = encryption_key {
|
||||||
// .parse::<SqliteConnectOptions>()?
|
connect_options = connect_options.pragma("key", format!("'{}'", encryption_key));
|
||||||
// .log_statements(log_statements_level)
|
}
|
||||||
// .read_only(read_only)
|
|
||||||
// .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
|
|
||||||
// .foreign_keys(true)
|
|
||||||
// .busy_timeout(Duration::from_millis(5000))
|
|
||||||
// .pragma("recursive_triggers", "ON")
|
|
||||||
// .log_slow_statements(tracing::log::LevelFilter::Warn, Duration::from_millis(500));
|
|
||||||
|
|
||||||
// let pool = SqlitePoolOptions::new()
|
let pool = SqlitePoolOptions::new()
|
||||||
// .acquire_timeout(Duration::from_secs(5))
|
.acquire_timeout(Duration::from_secs(5))
|
||||||
// .max_connections(10)
|
.max_connections(10)
|
||||||
// .connect_with(connect_options)
|
.connect_with(connect_options)
|
||||||
// .await?;
|
.await?;
|
||||||
|
|
||||||
// let row: (String, String) = sqlx::query_as("SELECT sqlite_version(), sqlite_source_id()")
|
sqlx::migrate!("./src/database/migrations")
|
||||||
// .fetch_one(&pool)
|
.run(&pool)
|
||||||
// .await?;
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("migration error: {:?}", e);
|
||||||
|
TwonlyError::Generic(format!("Migration error: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
// tracing::info!("Rust SQLite Version: {}", row.0);
|
Ok(Self { pool })
|
||||||
// tracing::info!("Rust SQLite Source ID: {}", row.1);
|
}
|
||||||
|
|
||||||
// Ok(Self { pool: pool })
|
pub(crate) async fn create_backup(
|
||||||
// }
|
&self,
|
||||||
// }
|
output_path: &str,
|
||||||
|
encryption_key: Option<&str>,
|
||||||
|
) -> Result<()> {
|
||||||
|
if let Some(key) = encryption_key {
|
||||||
|
let mut conn = self
|
||||||
|
.pool
|
||||||
|
.acquire()
|
||||||
|
.await
|
||||||
|
.map_err(|e| TwonlyError::Generic(e.to_string()))?;
|
||||||
|
|
||||||
|
sqlx::query("ATTACH DATABASE ? AS backup KEY ?")
|
||||||
|
.bind(output_path)
|
||||||
|
.bind(key)
|
||||||
|
.execute(&mut *conn)
|
||||||
|
.await
|
||||||
|
.map_err(|e| TwonlyError::Generic(format!("Attach failed: {}", e)))?;
|
||||||
|
|
||||||
|
sqlx::query("SELECT sqlcipher_export('backup')")
|
||||||
|
.execute(&mut *conn)
|
||||||
|
.await
|
||||||
|
.map_err(|e| TwonlyError::Generic(format!("Export failed: {}", e)))?;
|
||||||
|
|
||||||
|
sqlx::query("DETACH DATABASE backup")
|
||||||
|
.execute(&mut *conn)
|
||||||
|
.await
|
||||||
|
.map_err(|e| TwonlyError::Generic(format!("Detach failed: {}", e)))?;
|
||||||
|
} else {
|
||||||
|
sqlx::query("VACUUM INTO ?")
|
||||||
|
.bind(output_path)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| TwonlyError::Generic(format!("Backup failed: {}", e)))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::database::tables::received_messages::ReceivedMessage;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_database_encryption_and_migrations() {
|
||||||
|
let _ = pretty_env_logger::try_init();
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let db_path = dir.path().join("test.sqlite").display().to_string();
|
||||||
|
let key = "secure_password";
|
||||||
|
|
||||||
|
// 1. Create and initialize database with key
|
||||||
|
let db = Database::new(&db_path, Some(key), false).await.unwrap();
|
||||||
|
ReceivedMessage::insert(&db.pool, "sender1", b"hello world")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// 2. Try to open with WRONG key
|
||||||
|
let result = Database::new(&db_path, Some("wrong_password"), false).await;
|
||||||
|
assert!(
|
||||||
|
result.is_err(),
|
||||||
|
"Opening with wrong key should fail. If this passes, the database might not be encrypted!"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Open with CORRECT key again
|
||||||
|
let db = Database::new(&db_path, Some(key), false).await.unwrap();
|
||||||
|
let messages = ReceivedMessage::get_all(&db.pool).await.unwrap();
|
||||||
|
assert_eq!(messages.len(), 1);
|
||||||
|
assert_eq!(messages[0].sender_id, "sender1");
|
||||||
|
assert_eq!(messages[0].content, b"hello world");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_database_backup_encrypted() {
|
||||||
|
let _ = pretty_env_logger::try_init();
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let db_path = dir.path().join("test_enc.sqlite").display().to_string();
|
||||||
|
let backup_path = dir.path().join("backup_enc.sqlite").display().to_string();
|
||||||
|
let key = "secure_password";
|
||||||
|
|
||||||
|
let db = Database::new(&db_path, Some(key), false).await.unwrap();
|
||||||
|
ReceivedMessage::insert(&db.pool, "sender1", b"hello world")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
db.create_backup(&backup_path, Some(key)).await.unwrap();
|
||||||
|
|
||||||
|
// 1. Verify it cannot be opened with wrong key
|
||||||
|
let result = Database::new(&backup_path, Some("wrong_password"), false).await;
|
||||||
|
assert!(
|
||||||
|
result.is_err(),
|
||||||
|
"Encrypted backup should fail with wrong key"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Open backup with correct key and verify data
|
||||||
|
let backup_db = Database::new(&backup_path, Some(key), false).await.unwrap();
|
||||||
|
let messages = ReceivedMessage::get_all(&backup_db.pool).await.unwrap();
|
||||||
|
assert_eq!(messages.len(), 1);
|
||||||
|
assert_eq!(messages[0].sender_id, "sender1");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_database_backup_plaintext() {
|
||||||
|
let _ = pretty_env_logger::try_init();
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let db_path = dir.path().join("test_plain.sqlite").display().to_string();
|
||||||
|
let backup_path = dir.path().join("backup_plain.sqlite").display().to_string();
|
||||||
|
|
||||||
|
let db = Database::new(&db_path, None, false).await.unwrap();
|
||||||
|
ReceivedMessage::insert(&db.pool, "sender1", b"hello world")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
db.create_backup(&backup_path, None).await.unwrap();
|
||||||
|
|
||||||
|
// Open backup and verify
|
||||||
|
let backup_db = Database::new(&backup_path, None, false).await.unwrap();
|
||||||
|
let messages = ReceivedMessage::get_all(&backup_db.pool).await.unwrap();
|
||||||
|
assert_eq!(messages.len(), 1);
|
||||||
|
assert_eq!(messages[0].sender_id, "sender1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
2
rust/src/database/tables/mod.rs
Normal file
2
rust/src/database/tables/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod received_messages;
|
||||||
|
pub mod sending_messages;
|
||||||
34
rust/src/database/tables/received_messages.rs
Normal file
34
rust/src/database/tables/received_messages.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
use crate::error::Result;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use sqlx::{FromRow, SqlitePool};
|
||||||
|
|
||||||
|
#[derive(Debug, FromRow)]
|
||||||
|
pub struct ReceivedMessage {
|
||||||
|
pub id: i64,
|
||||||
|
pub sender_id: String,
|
||||||
|
pub content: Vec<u8>,
|
||||||
|
pub timestamp: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReceivedMessage {
|
||||||
|
pub async fn insert(pool: &SqlitePool, sender_id: &str, content: &[u8]) -> Result<i64> {
|
||||||
|
let result =
|
||||||
|
sqlx::query("INSERT INTO received_messages (sender_id, content) VALUES (?, ?)")
|
||||||
|
.bind(sender_id)
|
||||||
|
.bind(content)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(result.last_insert_rowid())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_all(pool: &SqlitePool) -> Result<Vec<Self>> {
|
||||||
|
let messages = sqlx::query_as::<_, Self>(
|
||||||
|
"SELECT id, sender_id, content, timestamp FROM received_messages ORDER BY timestamp DESC",
|
||||||
|
)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(messages)
|
||||||
|
}
|
||||||
|
}
|
||||||
35
rust/src/database/tables/sending_messages.rs
Normal file
35
rust/src/database/tables/sending_messages.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
use crate::error::Result;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use sqlx::{FromRow, SqlitePool};
|
||||||
|
|
||||||
|
#[derive(Debug, FromRow)]
|
||||||
|
pub struct SendingMessage {
|
||||||
|
pub id: i64,
|
||||||
|
pub recipient_id: String,
|
||||||
|
pub content: Vec<u8>,
|
||||||
|
pub timestamp: DateTime<Utc>,
|
||||||
|
pub status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SendingMessage {
|
||||||
|
pub async fn insert(pool: &SqlitePool, recipient_id: &str, content: &[u8]) -> Result<i64> {
|
||||||
|
let result =
|
||||||
|
sqlx::query("INSERT INTO sending_messages (recipient_id, content) VALUES (?, ?)")
|
||||||
|
.bind(recipient_id)
|
||||||
|
.bind(content)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(result.last_insert_rowid())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_all(pool: &SqlitePool) -> Result<Vec<Self>> {
|
||||||
|
let messages = sqlx::query_as::<_, Self>(
|
||||||
|
"SELECT id, recipient_id, content, timestamp, status FROM sending_messages ORDER BY timestamp DESC",
|
||||||
|
)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(messages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use protocols::user_discovery::error::UserDiscoveryError;
|
use protocols::user_discovery::error::UserDiscoveryError;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use zip::result::ZipError;
|
||||||
|
|
||||||
pub type Result<T> = core::result::Result<T, TwonlyError>;
|
pub type Result<T> = core::result::Result<T, TwonlyError>;
|
||||||
|
|
||||||
|
|
@ -7,6 +8,8 @@ pub type Result<T> = core::result::Result<T, TwonlyError>;
|
||||||
pub enum TwonlyError {
|
pub enum TwonlyError {
|
||||||
#[error("global twonly is not initialized")]
|
#[error("global twonly is not initialized")]
|
||||||
Initialization,
|
Initialization,
|
||||||
|
#[error("Tried to access the wrong context")]
|
||||||
|
WrongContext,
|
||||||
#[error("init_flutter_callbacks was not called")]
|
#[error("init_flutter_callbacks was not called")]
|
||||||
MissingCallbackInitialization,
|
MissingCallbackInitialization,
|
||||||
#[error("Could not find the given database")]
|
#[error("Could not find the given database")]
|
||||||
|
|
@ -15,8 +18,28 @@ pub enum TwonlyError {
|
||||||
UserDiscoveryError(#[from] UserDiscoveryError),
|
UserDiscoveryError(#[from] UserDiscoveryError),
|
||||||
#[error("Error in dart callback")]
|
#[error("Error in dart callback")]
|
||||||
DartError,
|
DartError,
|
||||||
|
#[error(
|
||||||
|
"Storage error: database exists but master key could not be loaded from secure storage"
|
||||||
|
)]
|
||||||
|
SecureStorageError,
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
SqliteError(#[from] sqlx::Error),
|
SqliteError(#[from] sqlx::Error),
|
||||||
|
#[error("{0}")]
|
||||||
|
Generic(String),
|
||||||
|
#[error("{0}")]
|
||||||
|
IoError(#[from] std::io::Error),
|
||||||
|
|
||||||
|
#[error("{0}")]
|
||||||
|
ZipError(#[from] ZipError),
|
||||||
|
|
||||||
|
#[error("{0}")]
|
||||||
|
Walkdir(#[from] walkdir::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for TwonlyError {
|
||||||
|
fn from(error: String) -> Self {
|
||||||
|
TwonlyError::Generic(error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<TwonlyError> for UserDiscoveryError {
|
impl From<TwonlyError> for UserDiscoveryError {
|
||||||
|
|
@ -195,7 +195,7 @@ fn wire__crate__bridge__initialize_twonly_flutter_impl(
|
||||||
};
|
};
|
||||||
let mut deserializer =
|
let mut deserializer =
|
||||||
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
let api_config = <crate::bridge::TwonlyConfig>::sse_decode(&mut deserializer);
|
let api_config = <crate::bridge::InitConfig>::sse_decode(&mut deserializer);
|
||||||
deserializer.end();
|
deserializer.end();
|
||||||
move |context| async move {
|
move |context| async move {
|
||||||
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>(
|
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>(
|
||||||
|
|
@ -743,6 +743,18 @@ impl SseDecode for i64 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SseDecode for crate::bridge::InitConfig {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
|
let mut var_databaseDir = <String>::sse_decode(deserializer);
|
||||||
|
let mut var_dataDir = <String>::sse_decode(deserializer);
|
||||||
|
return crate::bridge::InitConfig {
|
||||||
|
database_dir: var_databaseDir,
|
||||||
|
data_dir: var_dataDir,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SseDecode for isize {
|
impl SseDecode for isize {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
|
|
@ -863,18 +875,6 @@ impl SseDecode for crate::bridge::OtherPromotion {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SseDecode for crate::bridge::TwonlyConfig {
|
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
|
||||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
|
||||||
let mut var_databasePath = <String>::sse_decode(deserializer);
|
|
||||||
let mut var_dataDirectory = <String>::sse_decode(deserializer);
|
|
||||||
return crate::bridge::TwonlyConfig {
|
|
||||||
database_path: var_databasePath,
|
|
||||||
data_directory: var_dataDirectory,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SseDecode for u32 {
|
impl SseDecode for u32 {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
|
|
@ -985,6 +985,22 @@ impl flutter_rust_bridge::IntoIntoDart<crate::bridge::wrapper::user_discovery::F
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
|
impl flutter_rust_bridge::IntoDart for crate::bridge::InitConfig {
|
||||||
|
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
||||||
|
[
|
||||||
|
self.database_dir.into_into_dart().into_dart(),
|
||||||
|
self.data_dir.into_into_dart().into_dart(),
|
||||||
|
]
|
||||||
|
.into_dart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::bridge::InitConfig {}
|
||||||
|
impl flutter_rust_bridge::IntoIntoDart<crate::bridge::InitConfig> for crate::bridge::InitConfig {
|
||||||
|
fn into_into_dart(self) -> crate::bridge::InitConfig {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
impl flutter_rust_bridge::IntoDart for FrbWrapper<crate::bridge::OtherPromotion> {
|
impl flutter_rust_bridge::IntoDart for FrbWrapper<crate::bridge::OtherPromotion> {
|
||||||
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
||||||
[
|
[
|
||||||
|
|
@ -1012,24 +1028,6 @@ impl flutter_rust_bridge::IntoIntoDart<FrbWrapper<crate::bridge::OtherPromotion>
|
||||||
self.into()
|
self.into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
|
||||||
impl flutter_rust_bridge::IntoDart for crate::bridge::TwonlyConfig {
|
|
||||||
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
|
||||||
[
|
|
||||||
self.database_path.into_into_dart().into_dart(),
|
|
||||||
self.data_directory.into_into_dart().into_dart(),
|
|
||||||
]
|
|
||||||
.into_dart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::bridge::TwonlyConfig {}
|
|
||||||
impl flutter_rust_bridge::IntoIntoDart<crate::bridge::TwonlyConfig>
|
|
||||||
for crate::bridge::TwonlyConfig
|
|
||||||
{
|
|
||||||
fn into_into_dart(self) -> crate::bridge::TwonlyConfig {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SseEncode for flutter_rust_bridge::for_generated::anyhow::Error {
|
impl SseEncode for flutter_rust_bridge::for_generated::anyhow::Error {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
|
@ -1087,6 +1085,14 @@ impl SseEncode for i64 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SseEncode for crate::bridge::InitConfig {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
|
<String>::sse_encode(self.database_dir, serializer);
|
||||||
|
<String>::sse_encode(self.data_dir, serializer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SseEncode for isize {
|
impl SseEncode for isize {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
|
|
@ -1189,14 +1195,6 @@ impl SseEncode for crate::bridge::OtherPromotion {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SseEncode for crate::bridge::TwonlyConfig {
|
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
|
||||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
|
||||||
<String>::sse_encode(self.database_path, serializer);
|
|
||||||
<String>::sse_encode(self.data_directory, serializer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SseEncode for u32 {
|
impl SseEncode for u32 {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
|
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
struct TwonlyIdentity {}
|
|
||||||
|
|
||||||
struct NostrIdentity {}
|
|
||||||
|
|
||||||
struct KeyManager {
|
|
||||||
main_key: [u8; 32],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl KeyManager {
|
|
||||||
fn try_from_keychain() -> KeyManager {
|
|
||||||
todo!();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_new() {
|
|
||||||
// generates main_key
|
|
||||||
|
|
||||||
// generates signal identity
|
|
||||||
// generates nostr identity
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_signal_identity() {}
|
|
||||||
|
|
||||||
fn recover_from_trusted_friends() {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_backup_key() {}
|
|
||||||
|
|
||||||
fn recover_from_backup() {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
}
|
|
||||||
73
rust/src/keys/README.md
Normal file
73
rust/src/keys/README.md
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
# Cryptographic Architecture
|
||||||
|
|
||||||
|
## 1. Main Key
|
||||||
|
A cryptographically secure, immutable master key. Loss of this key, in the absence of valid backups, results in permanent loss of account access.
|
||||||
|
|
||||||
|
*Key Derivation*: Utilizes HKDF to derive subordinate keys.
|
||||||
|
|
||||||
|
- Authentication Token: Uploaded to the server for session authentication.
|
||||||
|
- Backup Key: Used to encrypt a backup
|
||||||
|
- Backup Content: The encrypted backup encompasses
|
||||||
|
- Main Key
|
||||||
|
- Identity keys
|
||||||
|
- Local database (including contacts, public identities, memories (only references), and messages).
|
||||||
|
- Lifecycle: Backups are refreshed daily and deleted after one year.
|
||||||
|
- Media Main Key: Used to wrap media-specific keys.
|
||||||
|
- A new, cryptographically secure key is generated for every media file.
|
||||||
|
- The media key is wrapped using AES-GCM and the Main Media Key and stored in the online database along side to the uploaded media file database entry.
|
||||||
|
- The original media file is encrypted using AES-GCM and uploaded to the designated storage bucket.
|
||||||
|
|
||||||
|
## 3. Identity Keys
|
||||||
|
- Signal Identity
|
||||||
|
- Generates a private and public key pair for secure communication.
|
||||||
|
- Nostr Identity
|
||||||
|
- Generates a private and public key pair for Nostr network interactions.
|
||||||
|
|
||||||
|
|
||||||
|
## 1. Backup Keys
|
||||||
|
Independent, securely generated keys used to wrap the primary backup key.
|
||||||
|
|
||||||
|
### 1.1. Password-Based Backup
|
||||||
|
1. Derivation
|
||||||
|
- Utilizes scrypt with the username as the salt (cost 65536) to derive a 64-byte sequence.
|
||||||
|
2. Allocation:
|
||||||
|
- 32 bytes: Backup ID, used as the identifier to locate the backup on the server.
|
||||||
|
- 32 bytes: Backup wrapper key.
|
||||||
|
3. Content
|
||||||
|
- The payload contains the main key required to generate the auth token and the backup key.
|
||||||
|
4. Operation
|
||||||
|
- The backup wrapper key encrypts the main key. The ciphertext is uploaded anonymously to the server, indexed by the Backup ID.
|
||||||
|
5. Security Measures
|
||||||
|
- The server enforces strict rate limiting per IP address to prevent brute-force attacks.
|
||||||
|
6. Lifecycle
|
||||||
|
- These backup keys require a monthly refresh; otherwise, they are scheduled for deletion after two years.
|
||||||
|
|
||||||
|
### 1.2. Trusted Friends Keys (Passwordless Recovery)
|
||||||
|
1. Initiation
|
||||||
|
- The recovering user generates a temporary ID (TempID) and a new ephemeral asymmetric key pair.
|
||||||
|
2. Request
|
||||||
|
- A recovery request containing the TempID and the public key is transmitted to a trusted contact via a secure link.
|
||||||
|
3. Verification
|
||||||
|
- The contact manually verifies the requestor's identity within their application to mitigate phishing risks.
|
||||||
|
4. Share Transmission
|
||||||
|
- The contact encrypts a trusted friend share using the provided public key. This share includes the user IDs, the minimum threshold required for decryption, and the cryptographic share (utilizing Shamir's Secret Sharing).
|
||||||
|
5. Reconstruction
|
||||||
|
- Upon receiving the required threshold of shares, the user reconstructs the shared secret data.
|
||||||
|
6. Second Factor (Optional)
|
||||||
|
- The shared secret data may mandate an additional factor (PIN or Email). For a PIN factor, an unlock token and a PIN seed are used to securely retrieve the remaining share from the server without exposing the raw PIN.
|
||||||
|
7. Final Recovery
|
||||||
|
- The decrypted recovery data provides the User ID, private key, and the backup master key necessary to restore the account and its backups.
|
||||||
|
|
||||||
|
## 4. Web Portal Upload Protocol
|
||||||
|
1. Initialization
|
||||||
|
- The web portal generates a cryptographically secure symmetric key for end-to-end encrypted (E2EE) communication with the mobile application, alongside a newly registered session token.
|
||||||
|
2. Handshake
|
||||||
|
- The mobile application scans the QR code containing the session token and the symmetric key.
|
||||||
|
3. Authorization
|
||||||
|
- The application signals readiness via the server using the session token and securely provisions a temporary authentication token for media uploads over the established symmetric E2EE channel.
|
||||||
|
4. Key Exchange
|
||||||
|
- The web portal encrypts the media file using a newly generated media key. It transmits this media key to the application (encrypted via the E2EE symmetric key) and receives the wrapped media key in return.
|
||||||
|
5. Upload
|
||||||
|
- The web portal uploads the encrypted media file to the server, assigning it a device ID of 0.
|
||||||
|
6. Synchronization
|
||||||
|
- Finally, the application requests all memories with a device ID lower than its current one (the device ID increments after a backup restoration).
|
||||||
0
rust/src/keys/backup_password.rs
Normal file
0
rust/src/keys/backup_password.rs
Normal file
1
rust/src/keys/backup_passwordless/mod.rs
Normal file
1
rust/src/keys/backup_passwordless/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
mod types;
|
||||||
83
rust/src/keys/backup_passwordless/types.rs
Normal file
83
rust/src/keys/backup_passwordless/types.rs
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Send from the person who tries to recover their account.
|
||||||
|
/// This can be done via a link, which will then be opened in the app of the contact.
|
||||||
|
/// The contact then has to manually select from which user he got the request.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct RecoveryRequest {
|
||||||
|
pub temp_id: i64,
|
||||||
|
pub public_key: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used as envelope for TrustedFriendShare and RecoveryData
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct EncryptedEnvelope {
|
||||||
|
pub encrypted_data: Vec<u8>,
|
||||||
|
pub iv: Vec<u8>,
|
||||||
|
pub mac: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct User {
|
||||||
|
pub user_id: i64,
|
||||||
|
pub display_name: String,
|
||||||
|
pub avatar: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send from the trusted friend.
|
||||||
|
/// This is encrypted with the received public key.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct TrustedFriendShare {
|
||||||
|
/// This allows to display the user which user has send him his recovery data.
|
||||||
|
pub trusted_friend: User,
|
||||||
|
/// This allows to display the userdata, showing that he is recovering the correct person.
|
||||||
|
pub share_user: User,
|
||||||
|
/// The minimum threshold required to decrypt the shares.
|
||||||
|
pub threshold: i32,
|
||||||
|
/// The actual share which will become: SecretSharedData
|
||||||
|
pub share: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct SecondFactorPin {
|
||||||
|
/// Required to try the PIN to get the share from the server.
|
||||||
|
/// This prevents that someone else can lock the pin, as the server only
|
||||||
|
/// allows 3 tries then after 1 day again 3 tries until the key is deleted.
|
||||||
|
pub unlock_token: Vec<u8>,
|
||||||
|
/// This never is send to the server but used to hash the pin before sending it to the server.
|
||||||
|
/// This prevents that the server every knows the short 4-digit PIN.
|
||||||
|
pub pin_seed: Vec<u8>,
|
||||||
|
/// The recovery data in case a second factor was used
|
||||||
|
/// The decryption key is loaded from the server either using the PIN or the MAIL
|
||||||
|
pub recovery_data_encrypted: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct SecondFactorMail {
|
||||||
|
/// The users selected mail which will be send to the server
|
||||||
|
/// To this mail the encryption key for the recovery_data is send
|
||||||
|
pub mail: String,
|
||||||
|
/// Required to try the PIN to get the share from the server.
|
||||||
|
/// This prevents that someone else can lock the pin, as the server only
|
||||||
|
/// allows 3 tries then after 1 day again 3 tries until the key is deleted.
|
||||||
|
pub unlock_token: Vec<u8>,
|
||||||
|
/// The recovery data in case a second factor was used
|
||||||
|
/// The decryption key is loaded from the server either using the PIN or the MAIL
|
||||||
|
pub recovery_data_encrypted: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub enum SecretSharedData {
|
||||||
|
None(RecoveryData),
|
||||||
|
Mail(SecondFactorMail),
|
||||||
|
Pin(SecondFactorPin),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The data which is recovered at the end.
|
||||||
|
/// The backup_master_key allows to recover the actual backup uploaded in the background to the server.
|
||||||
|
/// In case the backup is not available any more the user can use its user_id and his private_key to register as a new user.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct RecoveryData {
|
||||||
|
pub user_id: i64,
|
||||||
|
pub master_key: Vec<u8>,
|
||||||
|
}
|
||||||
8
rust/src/keys/identity_key.rs
Normal file
8
rust/src/keys/identity_key.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||||
|
|
||||||
|
#[derive(Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
|
||||||
|
pub(crate) enum IdentityKey {
|
||||||
|
Nost(),
|
||||||
|
Signal(),
|
||||||
|
}
|
||||||
256
rust/src/keys/main_key.rs
Normal file
256
rust/src/keys/main_key.rs
Normal file
|
|
@ -0,0 +1,256 @@
|
||||||
|
use crate::error::Result;
|
||||||
|
use aes_gcm::aead::rand_core::RngCore;
|
||||||
|
use aes_gcm::aead::{Aead, AeadCore, KeyInit, OsRng};
|
||||||
|
use aes_gcm::{Aes256Gcm, Key, Nonce};
|
||||||
|
use hkdf::Hkdf;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sha2::Sha256;
|
||||||
|
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||||
|
|
||||||
|
/// `MainKey` is responsible for handling the cryptographically secure, immutable master key.
|
||||||
|
/// It uses HKDF to derive subordinate keys (Authentication Token, Backup Key, Media Main Key).
|
||||||
|
#[derive(Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
|
||||||
|
pub struct MainKey {
|
||||||
|
/// The 32-byte main master key
|
||||||
|
main_key: [u8; 32],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) enum DatabaseKey {
|
||||||
|
RustDb,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MainKey {
|
||||||
|
/// Generates a new cryptographically secure MainKey.
|
||||||
|
pub fn generate() -> Self {
|
||||||
|
let mut main_key = [0u8; 32];
|
||||||
|
OsRng.fill_bytes(&mut main_key);
|
||||||
|
Self { main_key }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initializes a MainKey from an existing main key.
|
||||||
|
pub fn from_main_key(main_key: [u8; 32]) -> Self {
|
||||||
|
Self { main_key }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Derives the database encryption key.
|
||||||
|
pub(crate) fn get_database_key(&self, db: DatabaseKey) -> String {
|
||||||
|
let db_name = match db {
|
||||||
|
DatabaseKey::RustDb => b"rust_db",
|
||||||
|
};
|
||||||
|
let info = [b"database_key_", db_name as &[u8]].concat();
|
||||||
|
let key = self.derive_key(&info);
|
||||||
|
hex::encode(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Derives the authentication token uploaded to the server for session authentication.
|
||||||
|
pub fn get_authentication_token(&self) -> [u8; 32] {
|
||||||
|
self.derive_key(b"auth_token")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encrypts a backup payload.
|
||||||
|
/// The backup key is derived using HKDF from the main key.
|
||||||
|
pub fn encrypt_backup(&self, backup_payload: &[u8]) -> Vec<u8> {
|
||||||
|
self.encrypt_with_info(b"backup_key", backup_payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypts a backup payload.
|
||||||
|
pub fn decrypt_backup(&self, encrypted_backup: &[u8]) -> Result<Vec<u8>> {
|
||||||
|
self.decrypt_with_info(b"backup_key", encrypted_backup)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encrypts a newly generated media key using the derived Media Main Key.
|
||||||
|
pub fn encrypt_media_key(&self, media_key: &[u8; 32]) -> Vec<u8> {
|
||||||
|
self.encrypt_with_info(b"media_main_key", media_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypts a wrapped media key using the derived Media Main Key.
|
||||||
|
pub fn decrypt_media_key(&self, wrapped_media_key: &[u8]) -> Result<[u8; 32]> {
|
||||||
|
let decrypted = self.decrypt_with_info(b"media_main_key", wrapped_media_key)?;
|
||||||
|
|
||||||
|
if decrypted.len() != 32 {
|
||||||
|
return Err("Invalid decrypted key length".to_string())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut result = [0u8; 32];
|
||||||
|
result.copy_from_slice(&decrypted);
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn derive_key(&self, info: &[u8]) -> [u8; 32] {
|
||||||
|
let hk = Hkdf::<Sha256>::new(None, &self.main_key);
|
||||||
|
let mut okm = [0u8; 32];
|
||||||
|
hk.expand(info, &mut okm).expect("HKDF expand failed");
|
||||||
|
okm
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encrypt_with_info(&self, info: &[u8], payload: &[u8]) -> Vec<u8> {
|
||||||
|
let derived_key = self.derive_key(info);
|
||||||
|
let key = Key::<Aes256Gcm>::from_slice(&derived_key);
|
||||||
|
let cipher = Aes256Gcm::new(key);
|
||||||
|
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
|
||||||
|
let ciphertext = cipher
|
||||||
|
.encrypt(&nonce, payload)
|
||||||
|
.expect("encryption failure!");
|
||||||
|
|
||||||
|
let mut result = nonce.to_vec();
|
||||||
|
result.extend_from_slice(&ciphertext);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt_with_info(&self, info: &[u8], encrypted_data: &[u8]) -> Result<Vec<u8>> {
|
||||||
|
if encrypted_data.len() < 12 {
|
||||||
|
return Err("Invalid encrypted data length".to_string())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let derived_key = self.derive_key(info);
|
||||||
|
let key = Key::<Aes256Gcm>::from_slice(&derived_key);
|
||||||
|
let cipher = Aes256Gcm::new(key);
|
||||||
|
let nonce = Nonce::from_slice(&encrypted_data[..12]);
|
||||||
|
let ciphertext = &encrypted_data[12..];
|
||||||
|
|
||||||
|
Ok(cipher
|
||||||
|
.decrypt(nonce, ciphertext)
|
||||||
|
.map_err(|_| "Decryption failure".to_string())?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_generate_and_from_main_key() {
|
||||||
|
let km = MainKey::generate();
|
||||||
|
let km2 = MainKey::from_main_key(km.main_key);
|
||||||
|
assert_eq!(km.main_key, km2.main_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_authentication_token() {
|
||||||
|
let km1 = MainKey::generate();
|
||||||
|
let token1 = km1.get_authentication_token();
|
||||||
|
|
||||||
|
let km2 = MainKey::from_main_key(km1.main_key);
|
||||||
|
let token2 = km2.get_authentication_token();
|
||||||
|
|
||||||
|
// Tokens derived from the same main key should match
|
||||||
|
assert_eq!(token1, token2);
|
||||||
|
|
||||||
|
let km3 = MainKey::generate();
|
||||||
|
let token3 = km3.get_authentication_token();
|
||||||
|
|
||||||
|
// Different main keys should produce different tokens
|
||||||
|
assert_ne!(token1, token3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_backup_encryption_decryption_success() {
|
||||||
|
let km = MainKey::generate();
|
||||||
|
let payload = b"this is a secret backup payload";
|
||||||
|
|
||||||
|
let encrypted = km.encrypt_backup(payload);
|
||||||
|
let decrypted = km.decrypt_backup(&encrypted).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(payload.as_slice(), decrypted.as_slice());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_backup_decryption_tampered_payload_fails() {
|
||||||
|
let km = MainKey::generate();
|
||||||
|
let payload = b"this is a secret backup payload";
|
||||||
|
let mut encrypted = km.encrypt_backup(payload);
|
||||||
|
|
||||||
|
// Tamper with the ciphertext (assuming length > 12)
|
||||||
|
let last_idx = encrypted.len() - 1;
|
||||||
|
encrypted[last_idx] ^= 1; // Flip a bit
|
||||||
|
|
||||||
|
let result = km.decrypt_backup(&encrypted);
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(result.unwrap_err().to_string(), "Decryption failure");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_backup_decryption_too_short_fails() {
|
||||||
|
let km = MainKey::generate();
|
||||||
|
let short_payload = vec![0u8; 10]; // Less than 12 bytes nonce
|
||||||
|
|
||||||
|
let result = km.decrypt_backup(&short_payload);
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().to_string(),
|
||||||
|
"Invalid encrypted data length"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_media_key_encryption_decryption_success() {
|
||||||
|
let km = MainKey::generate();
|
||||||
|
let mut media_key = [0u8; 32];
|
||||||
|
OsRng.fill_bytes(&mut media_key);
|
||||||
|
|
||||||
|
let encrypted = km.encrypt_media_key(&media_key);
|
||||||
|
let decrypted = km.decrypt_media_key(&encrypted).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(media_key, decrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_media_key_decryption_tampered_payload_fails() {
|
||||||
|
let km = MainKey::generate();
|
||||||
|
let mut media_key = [0u8; 32];
|
||||||
|
OsRng.fill_bytes(&mut media_key);
|
||||||
|
|
||||||
|
let mut encrypted = km.encrypt_media_key(&media_key);
|
||||||
|
|
||||||
|
// Tamper with the ciphertext
|
||||||
|
let last_idx = encrypted.len() - 1;
|
||||||
|
encrypted[last_idx] ^= 1;
|
||||||
|
|
||||||
|
let result = km.decrypt_media_key(&encrypted);
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(result.unwrap_err().to_string(), "Decryption failure");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_media_key_decryption_too_short_fails() {
|
||||||
|
let km = MainKey::generate();
|
||||||
|
let short_payload = vec![0u8; 10]; // Less than 12 bytes nonce
|
||||||
|
|
||||||
|
let result = km.decrypt_media_key(&short_payload);
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().to_string(),
|
||||||
|
"Invalid encrypted data length"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_media_key_decryption_wrong_decrypted_length_fails() {
|
||||||
|
let km = MainKey::generate();
|
||||||
|
|
||||||
|
// Manually encrypt a 31 byte payload
|
||||||
|
let hk = Hkdf::<Sha256>::new(None, &km.main_key);
|
||||||
|
let mut media_main_key = [0u8; 32];
|
||||||
|
hk.expand(b"media_main_key", &mut media_main_key)
|
||||||
|
.expect("HKDF expand failed");
|
||||||
|
|
||||||
|
let key = Key::<Aes256Gcm>::from_slice(&media_main_key);
|
||||||
|
let cipher = Aes256Gcm::new(key);
|
||||||
|
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
|
||||||
|
let payload = vec![0u8; 31];
|
||||||
|
let ciphertext = cipher
|
||||||
|
.encrypt(&nonce, payload.as_ref())
|
||||||
|
.expect("encryption failure");
|
||||||
|
|
||||||
|
let mut encrypted = nonce.to_vec();
|
||||||
|
encrypted.extend_from_slice(&ciphertext);
|
||||||
|
|
||||||
|
let result = km.decrypt_media_key(&encrypted);
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err().to_string(),
|
||||||
|
"Invalid decrypted key length"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
rust/src/keys/mod.rs
Normal file
52
rust/src/keys/mod.rs
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
mod backup_password;
|
||||||
|
mod backup_passwordless;
|
||||||
|
mod identity_key;
|
||||||
|
mod main_key;
|
||||||
|
|
||||||
|
pub(crate) use crate::keys::main_key::{DatabaseKey, MainKey};
|
||||||
|
use crate::secure_storage::SecureStorage;
|
||||||
|
use crate::{error::Result, keys::identity_key::IdentityKey};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||||
|
|
||||||
|
const KEY_MANAGER_ID: &str = "twonly_key_manager";
|
||||||
|
|
||||||
|
#[derive(Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
|
||||||
|
pub(crate) struct KeyManager {
|
||||||
|
pub(crate) main_key: MainKey,
|
||||||
|
pub(crate) identity_keys: Vec<IdentityKey>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyManager {
|
||||||
|
pub fn generate() -> Result<Self> {
|
||||||
|
Ok(KeyManager {
|
||||||
|
main_key: MainKey::generate(),
|
||||||
|
identity_keys: vec![],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tries to load the KeyManager from the secure keychain/local storage.
|
||||||
|
pub fn try_from_keychain(storage: &SecureStorage) -> Result<Self> {
|
||||||
|
let hex_key = storage
|
||||||
|
.read(KEY_MANAGER_ID)?
|
||||||
|
.ok_or_else(|| "Main key not found in keychain".to_string())?;
|
||||||
|
|
||||||
|
let bytes = hex::decode(hex_key).map_err(|e| format!("Failed to decode hex key: {}", e))?;
|
||||||
|
|
||||||
|
let main_key: KeyManager = postcard::from_bytes(&bytes)
|
||||||
|
.map_err(|e| format!("Failed to deserialize KeyManager: {}", e))?;
|
||||||
|
|
||||||
|
Ok(main_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stores the main key into the secure keychain/local storage.
|
||||||
|
pub fn store_to_keychain(&self, storage: &SecureStorage) -> Result<()> {
|
||||||
|
let serialized = postcard::to_allocvec(self)
|
||||||
|
.map_err(|e| format!("Failed to serialize KeyManager: {}", e))?;
|
||||||
|
|
||||||
|
let hex_key = hex::encode(serialized);
|
||||||
|
storage.write(KEY_MANAGER_ID, &hex_key)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
|
mod backup;
|
||||||
pub mod bridge;
|
pub mod bridge;
|
||||||
|
mod context;
|
||||||
mod database;
|
mod database;
|
||||||
|
mod error;
|
||||||
mod frb_generated;
|
mod frb_generated;
|
||||||
|
mod keys;
|
||||||
|
mod log;
|
||||||
|
mod secure_storage;
|
||||||
mod standalone;
|
mod standalone;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ fn build_writers(logs_dir: &std::path::Path) -> (NonBlocking, NonBlocking) {
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Failed to create file appender: {}", e);
|
eprintln!("Failed to create file appender: {}", e);
|
||||||
let (nb, guard) = tracing_appender::non_blocking(std::io::sink());
|
let (nb, _guard) = tracing_appender::non_blocking(std::io::sink());
|
||||||
(nb, None)
|
(nb, None)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
132
rust/src/secure_storage.rs
Normal file
132
rust/src/secure_storage.rs
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
use keyring_core::{Entry, Error as KeyringError};
|
||||||
|
|
||||||
|
/// A simple wrapper around `keyring-core` for secure storage on iOS, Android, and other platforms.
|
||||||
|
///
|
||||||
|
/// IMPORTANT: This struct assumes that a `keyring-core` default store has been initialized
|
||||||
|
/// (e.g., via `keyring_core::set_default_store`). In the White Noise project, this is handled
|
||||||
|
/// during application startup in `Whitenoise::initialize_keyring_store`.
|
||||||
|
pub struct SecureStorage {
|
||||||
|
service_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SecureStorage {
|
||||||
|
/// Creates a new `SecureStorage` instance with the specified service name.
|
||||||
|
/// The service name is used as a namespace in the system keyring.
|
||||||
|
pub fn new(service_name: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
service_name: service_name.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initializes the platform-native secure storage backend for iOS and Android.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `group_id` - (iOS only) Optional App Group ID to allow cross-process keychain access.
|
||||||
|
///
|
||||||
|
/// This function registers the appropriate credential store (Protected Store for iOS,
|
||||||
|
/// Keystore for Android) with `keyring-core`. It is safe to call multiple times.
|
||||||
|
pub fn init() -> Result<(), String> {
|
||||||
|
if keyring_core::get_default_store().is_some() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "ios")]
|
||||||
|
{
|
||||||
|
let group = "CN332ZUGRP.eu.twonly.shared";
|
||||||
|
let store = apple_native_keyring_store::protected::Store::with_application_group(group)
|
||||||
|
.map_err(|e| format!("Failed to init iOS Protected Store: {}", e))?;
|
||||||
|
keyring_core::set_default_store(store);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
{
|
||||||
|
let store = android_native_keyring_store::Store::new()
|
||||||
|
.map_err(|e| format!("Failed to init Android Store: {}", e))?;
|
||||||
|
keyring_core::set_default_store(store);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "ios", target_os = "android")))]
|
||||||
|
{
|
||||||
|
let store = keyring_core::mock::Store::new()
|
||||||
|
.map_err(|e| format!("Failed to init Mock Store: {}", e))?;
|
||||||
|
keyring_core::set_default_store(store);
|
||||||
|
tracing::warn!("Using mock store as default keyring store!");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes a secret value to the secure keyring associated with the given key.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `key` - The identifier (account name) for the secret.
|
||||||
|
/// * `value` - The secret string to store.
|
||||||
|
pub fn write(&self, key: &str, value: &str) -> Result<(), String> {
|
||||||
|
let entry = Entry::new(&self.service_name, key)
|
||||||
|
.map_err(|e| format!("Failed to create keyring entry: {}", e))?;
|
||||||
|
|
||||||
|
entry
|
||||||
|
.set_password(value)
|
||||||
|
.map_err(|e| format!("Failed to write secret to keyring: {}", e))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads a secret value from the secure keyring associated with the given key.
|
||||||
|
///
|
||||||
|
/// Returns `Ok(Some(String))` if the key exists, `Ok(None)` if it doesn't,
|
||||||
|
/// or an `Err` if a system error occurs.
|
||||||
|
pub fn read(&self, key: &str) -> Result<Option<String>, String> {
|
||||||
|
let entry = Entry::new(&self.service_name, key)
|
||||||
|
.map_err(|e| format!("Failed to create keyring entry: {}", e))?;
|
||||||
|
|
||||||
|
match entry.get_password() {
|
||||||
|
Ok(password) => Ok(Some(password)),
|
||||||
|
Err(KeyringError::NoEntry) => Ok(None),
|
||||||
|
Err(e) => Err(format!("Failed to read secret from keyring: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deletes the secret associated with the given key from the secure keyring.
|
||||||
|
///
|
||||||
|
/// If the key does not exist, this function returns `Ok(())` (idempotent).
|
||||||
|
pub fn delete(&self, key: &str) -> Result<(), String> {
|
||||||
|
let entry = Entry::new(&self.service_name, key)
|
||||||
|
.map_err(|e| format!("Failed to create keyring entry: {}", e))?;
|
||||||
|
|
||||||
|
match entry.delete_credential() {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
|
Err(KeyringError::NoEntry) => Ok(()),
|
||||||
|
Err(e) => Err(format!("Failed to delete secret from keyring: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_secure_storage_flow() {
|
||||||
|
// Initialize the store (will use MockStore on non-mobile platforms)
|
||||||
|
SecureStorage::init().unwrap();
|
||||||
|
|
||||||
|
let storage = SecureStorage::new("eu.twonly.test");
|
||||||
|
let key = "test_secret_key";
|
||||||
|
let secret = "my_awesome_secret_123";
|
||||||
|
|
||||||
|
// 1. Write the secret
|
||||||
|
storage.write(key, secret).expect("Failed to write secret");
|
||||||
|
|
||||||
|
// 2. Read the secret and verify it matches
|
||||||
|
let read_val = storage.read(key).expect("Failed to read secret");
|
||||||
|
assert_eq!(read_val, Some(secret.to_string()));
|
||||||
|
|
||||||
|
// 3. Delete the secret
|
||||||
|
storage.delete(key).expect("Failed to delete secret");
|
||||||
|
|
||||||
|
// 4. Verify the secret is gone
|
||||||
|
let after_delete = storage.read(key).expect("Failed to read after delete");
|
||||||
|
assert_eq!(after_delete, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
rust/src/standalone.rs
Normal file
11
rust/src/standalone.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
use crate::bridge::InitConfig;
|
||||||
|
use crate::database::Database;
|
||||||
|
use crate::secure_storage::SecureStorage;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub(crate) struct TwonlyStandalone {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(crate) config: InitConfig,
|
||||||
|
pub(crate) rust_db: Arc<Database>,
|
||||||
|
pub(crate) secure_storage: SecureStorage,
|
||||||
|
}
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
// use crate::{bridge::TwonlyConfig, database::Database};
|
|
||||||
// use std::sync::Arc;
|
|
||||||
|
|
||||||
// pub(crate) struct TwonlyStandalone {
|
|
||||||
// #[allow(dead_code)]
|
|
||||||
// pub(crate) config: TwonlyConfig,
|
|
||||||
// /// Because Rust is called from a different process it is safe to write to the twonly_db.
|
|
||||||
// pub(crate) twonly_db: Arc<Database>,
|
|
||||||
// }
|
|
||||||
Loading…
Reference in a new issue