mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-05-25 00:52:12 +00:00
keyring works
This commit is contained in:
parent
f323bc03eb
commit
5fa253ec32
29 changed files with 579 additions and 211 deletions
|
|
@ -6,6 +6,8 @@ import dev.darttools.flutter_android_volume_keydown.FlutterAndroidVolumeKeydownP
|
|||
import android.view.KeyEvent.KEYCODE_VOLUME_DOWN
|
||||
import android.view.KeyEvent.KEYCODE_VOLUME_UP
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import android.content.Context
|
||||
import io.crates.keyring.Keyring
|
||||
|
||||
class MainActivity : FlutterFragmentActivity() {
|
||||
|
||||
|
|
@ -24,6 +26,8 @@ class MainActivity : FlutterFragmentActivity() {
|
|||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
Keyring.initializeNdkContext(applicationContext)
|
||||
|
||||
MediaStoreChannel.configure(flutterEngine, applicationContext)
|
||||
VideoCompressionChannel.configure(flutterEngine, applicationContext)
|
||||
}
|
||||
|
|
|
|||
14
android/app/src/main/kotlin/io/crates/keyring/Keyring.kt
Normal file
14
android/app/src/main/kotlin/io/crates/keyring/Keyring.kt
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
package io.crates.keyring
|
||||
|
||||
import android.content.Context
|
||||
|
||||
class Keyring {
|
||||
companion object {
|
||||
init {
|
||||
// Replace with the name of your compiled Rust library
|
||||
System.loadLibrary("rust_lib_twonly")
|
||||
}
|
||||
// The underlying Rust crate provides the implementation for this
|
||||
external fun initializeNdkContext(context: Context)
|
||||
}
|
||||
}
|
||||
|
|
@ -38,9 +38,9 @@ final _initMutex = Mutex();
|
|||
|
||||
/// This function is used to initialized the absolute minimum so it
|
||||
/// can also be used by the backend without the UI was loaded.
|
||||
Future<void> twonlyMinimumInitialization() async {
|
||||
Future<bool> twonlyMinimumInitialization() async {
|
||||
Log.info('twonlyMinimumInitialization: called');
|
||||
await exclusiveAccess(
|
||||
final hasStorageError = await exclusiveAccess(
|
||||
lockName: 'init',
|
||||
mutex: _initMutex,
|
||||
action: () async {
|
||||
|
|
@ -54,15 +54,22 @@ Future<void> twonlyMinimumInitialization() async {
|
|||
await initFlutterCallbacksForRust();
|
||||
|
||||
Log.info('twonlyMinimumInitialization: bridge.initializeTwonlyFlutter()');
|
||||
await bridge.initializeTwonlyFlutter(
|
||||
config: bridge.InitConfig(
|
||||
databaseDir: AppEnvironment.supportDir,
|
||||
dataDir: AppEnvironment.supportDir,
|
||||
),
|
||||
);
|
||||
try {
|
||||
await bridge.initializeTwonlyFlutter(
|
||||
config: bridge.InitConfig(
|
||||
databaseDir: AppEnvironment.supportDir,
|
||||
dataDir: AppEnvironment.supportDir,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
return true;
|
||||
}
|
||||
Log.info('twonlyMinimumInitialization: finished');
|
||||
return false;
|
||||
},
|
||||
);
|
||||
return hasStorageError;
|
||||
}
|
||||
|
||||
void main() async {
|
||||
|
|
@ -72,18 +79,18 @@ void main() async {
|
|||
|
||||
unawaited(StartupGuard.markAppStartup());
|
||||
|
||||
await twonlyMinimumInitialization();
|
||||
|
||||
var storageError = await twonlyMinimumInitialization();
|
||||
unawaited(initFCMService());
|
||||
|
||||
var userExists = false;
|
||||
var storageError = false;
|
||||
|
||||
try {
|
||||
userExists = await userService.tryInit();
|
||||
} catch (e) {
|
||||
Log.error('Failed to initialize user session due to storage error: $e');
|
||||
storageError = true;
|
||||
if (!storageError) {
|
||||
try {
|
||||
userExists = await userService.tryInit();
|
||||
} catch (e) {
|
||||
Log.error('Failed to initialize user session due to storage error: $e');
|
||||
storageError = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (Platform.isIOS && userExists) {
|
||||
|
|
|
|||
|
|
@ -1511,7 +1511,7 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @backupPasswordRequirement.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Password must be at least 8 characters long.'**
|
||||
/// **'Password must be at least 10 characters long.'**
|
||||
String get backupPasswordRequirement;
|
||||
|
||||
/// No description provided for @backupExpertSettings.
|
||||
|
|
|
|||
|
|
@ -777,7 +777,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get backupPasswordRequirement =>
|
||||
'Das Passwort muss mindestens 8 Zeichen lang sein.';
|
||||
'Das Passwort muss mindestens 10 Zeichen lang sein.';
|
||||
|
||||
@override
|
||||
String get backupExpertSettings => 'Experteneinstellungen';
|
||||
|
|
|
|||
|
|
@ -771,7 +771,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get backupPasswordRequirement =>
|
||||
'Password must be at least 8 characters long.';
|
||||
'Password must be at least 10 characters long.';
|
||||
|
||||
@override
|
||||
String get backupExpertSettings => 'Expert settings';
|
||||
|
|
|
|||
|
|
@ -108,10 +108,9 @@ Future<void> handleBackupData(
|
|||
key: SecureStorageKeys.signalSignedPreKey,
|
||||
value: secureStorage[SecureStorageKeys.signalSignedPreKey] as String,
|
||||
);
|
||||
await storage.write(
|
||||
key: SecureStorageKeys.userData,
|
||||
value: secureStorage[SecureStorageKeys.userData] as String,
|
||||
);
|
||||
final userDataMap = jsonDecode(secureStorage[SecureStorageKeys.userData] as String) as Map<String, dynamic>;
|
||||
final userData = UserData.fromJson(userDataMap);
|
||||
await UserService.save(userData);
|
||||
await UserService.update((u) {
|
||||
u.deviceId += 1;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:twonly/src/constants/secure_storage.keys.dart';
|
|||
import 'package:twonly/src/model/json/userdata.model.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/secure_storage.dart';
|
||||
import 'package:twonly/src/utils/keyvalue.dart';
|
||||
|
||||
class UserService {
|
||||
late UserData currentUser;
|
||||
|
|
@ -26,21 +27,42 @@ class UserService {
|
|||
|
||||
static Future<UserData?> getUser() async {
|
||||
try {
|
||||
// 1. Try to load from KeyValueStore (user.json)
|
||||
final userDataMap = await KeyValueStore.get('user');
|
||||
if (userDataMap != null) {
|
||||
return UserData.fromJson(userDataMap);
|
||||
}
|
||||
|
||||
// 2. If not found, try to load from SecureStorage (Migration path)
|
||||
final userDataJson = await SecureStorage.instance.read(
|
||||
key: SecureStorageKeys.userData,
|
||||
);
|
||||
if (userDataJson == null) {
|
||||
return null;
|
||||
|
||||
if (userDataJson != null) {
|
||||
final userData = UserData.fromJson(
|
||||
jsonDecode(userDataJson) as Map<String, dynamic>,
|
||||
);
|
||||
|
||||
// 3. Run migration
|
||||
await _migrateFromSecureStorage(userData);
|
||||
return userData;
|
||||
}
|
||||
return UserData.fromJson(
|
||||
jsonDecode(userDataJson) as Map<String, dynamic>,
|
||||
);
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
Log.error('could not load user: $e');
|
||||
rethrow; // Rethrow instead of returning null to distinguish error from missing user
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _migrateFromSecureStorage(UserData userData) async {
|
||||
// Currently empty migration logic as requested, but we MUST store the data
|
||||
await KeyValueStore.put('user', userData.toJson());
|
||||
|
||||
// Optional: Log migration
|
||||
Log.info('Migrated user data from SecureStorage to KeyValueStore');
|
||||
}
|
||||
|
||||
static Future<void> update(
|
||||
void Function(UserData userData) updateUser,
|
||||
) async {
|
||||
|
|
@ -53,10 +75,7 @@ class UserService {
|
|||
user.defaultShowTime = null;
|
||||
}
|
||||
updateUser(user);
|
||||
await SecureStorage.instance.write(
|
||||
key: SecureStorageKeys.userData,
|
||||
value: jsonEncode(user),
|
||||
);
|
||||
await KeyValueStore.put('user', user.toJson());
|
||||
userService.currentUser = user;
|
||||
} catch (e) {
|
||||
Log.error('Could not update the user: $e');
|
||||
|
|
@ -66,6 +85,11 @@ class UserService {
|
|||
userService.triggerUserUpdate();
|
||||
}
|
||||
|
||||
static Future<void> save(UserData user) async {
|
||||
await KeyValueStore.put('user', user.toJson());
|
||||
await userService.tryInit();
|
||||
}
|
||||
|
||||
void triggerUserUpdate() {
|
||||
_userDataUpdateController.add(null);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,19 @@
|
|||
// ignore_for_file: avoid_dynamic_calls
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
||||
import 'package:twonly/src/model/json/userdata.model.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
||||
import 'package:twonly/src/services/signal/identity.signal.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/pow.dart';
|
||||
import 'package:twonly/src/utils/secure_storage.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||
import 'package:twonly/src/visual/views/groups/group.view.dart';
|
||||
|
|
@ -141,12 +138,7 @@ class _RegisterViewState extends State<RegisterView> {
|
|||
currentSetupPage: SetupPages.profile.name,
|
||||
)..appVersion = AppState.latestAppVersionId;
|
||||
|
||||
await SecureStorage.instance.write(
|
||||
key: SecureStorageKeys.userData,
|
||||
value: jsonEncode(userData),
|
||||
);
|
||||
|
||||
await userService.tryInit();
|
||||
await UserService.save(userData);
|
||||
|
||||
await apiService.authenticate();
|
||||
widget.callbackOnSuccess();
|
||||
|
|
|
|||
98
rust/Cargo.lock
generated
98
rust/Cargo.lock
generated
|
|
@ -34,7 +34,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cipher 0.4.4",
|
||||
"cpufeatures 0.2.17",
|
||||
]
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
|
|||
dependencies = [
|
||||
"aead",
|
||||
"aes",
|
||||
"cipher",
|
||||
"cipher 0.4.4",
|
||||
"ctr",
|
||||
"ghash",
|
||||
"subtle",
|
||||
|
|
@ -357,7 +357,7 @@ version = "0.1.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
|
||||
dependencies = [
|
||||
"cipher",
|
||||
"cipher 0.4.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -389,7 +389,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cipher 0.4.4",
|
||||
"cpufeatures 0.2.17",
|
||||
]
|
||||
|
||||
|
|
@ -412,7 +412,7 @@ checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
|
|||
dependencies = [
|
||||
"aead",
|
||||
"chacha20 0.9.1",
|
||||
"cipher",
|
||||
"cipher 0.4.4",
|
||||
"poly1305",
|
||||
"zeroize",
|
||||
]
|
||||
|
|
@ -438,10 +438,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||
dependencies = [
|
||||
"crypto-common 0.1.7",
|
||||
"inout",
|
||||
"inout 0.1.4",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e34d8227fe1ba289043aeb13792056ff80fd6de1a9f49137a5f499de8e8c78ea"
|
||||
dependencies = [
|
||||
"block-buffer 0.12.0",
|
||||
"crypto-common 0.2.1",
|
||||
"inout 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cmov"
|
||||
version = "0.5.3"
|
||||
|
|
@ -660,7 +671,7 @@ version = "0.9.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
|
||||
dependencies = [
|
||||
"cipher",
|
||||
"cipher 0.4.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1711,6 +1722,15 @@ dependencies = [
|
|||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7"
|
||||
dependencies = [
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.13"
|
||||
|
|
@ -2095,6 +2115,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"openssl-sys",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
|
@ -2287,7 +2308,7 @@ dependencies = [
|
|||
"getrandom 0.2.17",
|
||||
"hex",
|
||||
"instant",
|
||||
"scrypt",
|
||||
"scrypt 0.11.0",
|
||||
"secp256k1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
|
@ -2470,6 +2491,28 @@ dependencies = [
|
|||
"tls_codec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-src"
|
||||
version = "300.5.0+3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8ce546f549326b0e6052b649198487d91320875da901e7bd11a06d1ee3f9c2f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.115"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"openssl-src",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oslog"
|
||||
version = "0.2.0"
|
||||
|
|
@ -2565,6 +2608,16 @@ dependencies = [
|
|||
"hmac 0.12.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629"
|
||||
dependencies = [
|
||||
"digest 0.11.2",
|
||||
"hmac 0.13.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem-rfc7468"
|
||||
version = "0.7.0"
|
||||
|
|
@ -3134,6 +3187,7 @@ dependencies = [
|
|||
"prost-build",
|
||||
"protocols",
|
||||
"rand 0.10.1",
|
||||
"scrypt 0.12.0",
|
||||
"serde",
|
||||
"sha2 0.10.9",
|
||||
"sqlx",
|
||||
|
|
@ -3194,7 +3248,17 @@ version = "0.10.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
|
||||
dependencies = [
|
||||
"cipher",
|
||||
"cipher 0.4.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "salsa20"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f874456e72520ff1375a06c588eaf074b0f01f9e9e1aada45bd9b7954a6e42c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3219,11 +3283,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f"
|
||||
dependencies = [
|
||||
"password-hash",
|
||||
"pbkdf2",
|
||||
"salsa20",
|
||||
"pbkdf2 0.12.2",
|
||||
"salsa20 0.10.2",
|
||||
"sha2 0.10.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scrypt"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d87af57419b594aa23fa95f09f0e06d80d84ba01c26148c43844cad6ff4485f0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"pbkdf2 0.13.0",
|
||||
"salsa20 0.11.0",
|
||||
"sha2 0.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sec1"
|
||||
version = "0.7.3"
|
||||
|
|
|
|||
|
|
@ -24,7 +24,9 @@ mdk-core = { version = "0.8.0", git = "https://github.com/marmot-protocol/mdk",
|
|||
] }
|
||||
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"] }
|
||||
libsqlite3-sys = { version = "0.35.0", features = [
|
||||
"bundled-sqlcipher-vendored-openssl",
|
||||
] }
|
||||
tokio = { version = "1.44", features = ["full"] }
|
||||
tracing = "0.1.44"
|
||||
rand = "0.10.1"
|
||||
|
|
@ -42,6 +44,7 @@ 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"] }
|
||||
scrypt = { version = "0.12", default-features = false }
|
||||
walkdir = "2.5.0"
|
||||
[target.'cfg(target_os = "ios")'.dependencies]
|
||||
# iOS backend: Requires the 'protected' feature for Data Protection Keychain
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use crate::context::Context;
|
||||
use crate::database::Database;
|
||||
use crate::error::Result;
|
||||
use crate::keys::DatabaseKey;
|
||||
use crate::error::{Result, TwonlyError};
|
||||
use crate::keys::{DatabaseKey, MainKey};
|
||||
use std::fs::{remove_file, File};
|
||||
use std::io::{copy, Cursor};
|
||||
use std::path::PathBuf;
|
||||
|
|
@ -23,7 +23,8 @@ impl BackupArchive {
|
|||
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),
|
||||
("user_discovery_config.json", data_dir.clone(), false, None),
|
||||
("user.json", data_dir.join("keyvalue"), false, None),
|
||||
])
|
||||
}
|
||||
|
||||
|
|
@ -38,7 +39,7 @@ impl BackupArchive {
|
|||
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);
|
||||
let file_path = source_dir.join(file_name);
|
||||
if !file_path.exists() {
|
||||
tracing::warn!(
|
||||
"Could not backup {} as it does not exist.",
|
||||
|
|
@ -54,16 +55,20 @@ impl BackupArchive {
|
|||
encryption_key.is_none(),
|
||||
)
|
||||
.await?;
|
||||
let backup_database_file = backup_data_dir.join(&file_name).display().to_string();
|
||||
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);
|
||||
let file_backup = backup_data_dir.join(file_name);
|
||||
std::fs::copy(file_path, file_backup)?;
|
||||
}
|
||||
encryption_key.zeroize();
|
||||
}
|
||||
|
||||
let mut keys = ctx.get_key_manager()?;
|
||||
|
||||
let keys_serialized = postcard::to_allocvec(&keys)?;
|
||||
|
||||
let mut zip_data = Vec::new();
|
||||
|
||||
{
|
||||
|
|
@ -71,6 +76,9 @@ impl BackupArchive {
|
|||
let options =
|
||||
SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
|
||||
|
||||
zip.start_file(".key_manager.bin", options)?;
|
||||
copy(&mut keys_serialized.as_slice(), &mut zip)?;
|
||||
|
||||
for entry in WalkDir::new(&backup_data_dir) {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
|
@ -87,8 +95,6 @@ impl BackupArchive {
|
|||
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))?;
|
||||
|
||||
|
|
@ -98,13 +104,21 @@ impl BackupArchive {
|
|||
Ok(zip_path)
|
||||
}
|
||||
|
||||
pub(crate) async fn restore_from_backup(ctx: &Context, file_path: &PathBuf) -> Result<()> {
|
||||
pub(crate) async fn restore_from_backup(
|
||||
ctx: &Context,
|
||||
main_key_bytes: &[u8],
|
||||
file_path: &PathBuf,
|
||||
) -> Result<()> {
|
||||
let data_dir = PathBuf::from(&ctx.get_config()?.data_dir);
|
||||
|
||||
let mut keys = ctx.get_key_manager()?;
|
||||
let main_key_arr: [u8; 32] = main_key_bytes
|
||||
.try_into()
|
||||
.map_err(|_| TwonlyError::Generic("Invalid main key length".to_string()))?;
|
||||
|
||||
let mut main_key = MainKey::from_main_key(main_key_arr);
|
||||
|
||||
let encrypted_zip = std::fs::read(file_path)?;
|
||||
let zip_content = keys.main_key.decrypt_backup(&encrypted_zip)?;
|
||||
let zip_content = main_key.decrypt_backup(&encrypted_zip)?;
|
||||
|
||||
let restore_temp_dir = data_dir.join("restore_temp");
|
||||
|
||||
|
|
@ -120,6 +134,15 @@ impl BackupArchive {
|
|||
let mut file = archive.by_index(i)?;
|
||||
|
||||
if file.is_file() {
|
||||
let name = file.name().to_string();
|
||||
if name == ".key_manager.bin" {
|
||||
let mut data = Vec::new();
|
||||
copy(&mut file, &mut data)?;
|
||||
let key_manager: crate::keys::KeyManager = postcard::from_bytes(&data)?;
|
||||
key_manager.store_to_keychain(ctx.get_secure_storage()?)?;
|
||||
continue;
|
||||
}
|
||||
|
||||
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);
|
||||
|
|
@ -129,9 +152,9 @@ impl BackupArchive {
|
|||
}
|
||||
|
||||
for (file_name, target_dir, is_db, _) in Self::get_backup_files(ctx)? {
|
||||
let src = restore_temp_dir.join(&file_name);
|
||||
let src = restore_temp_dir.join(file_name);
|
||||
if src.exists() {
|
||||
let dst = target_dir.join(&file_name);
|
||||
let dst = target_dir.join(file_name);
|
||||
if is_db {
|
||||
// Remove existing database and its temporary files (WAL, SHM)
|
||||
let _ = remove_file(&dst);
|
||||
|
|
@ -143,7 +166,7 @@ impl BackupArchive {
|
|||
}
|
||||
}
|
||||
|
||||
keys.zeroize();
|
||||
main_key.zeroize();
|
||||
std::fs::remove_dir_all(&restore_temp_dir)?;
|
||||
|
||||
Ok(())
|
||||
|
|
@ -152,6 +175,8 @@ impl BackupArchive {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{database::tables::received_messages::ReceivedMessage, keys::KeyManager};
|
||||
|
||||
use super::*;
|
||||
use tempfile::tempdir;
|
||||
|
||||
|
|
@ -172,7 +197,14 @@ mod tests {
|
|||
{
|
||||
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 mut key_manager = ctx.get_key_manager().unwrap();
|
||||
key_manager
|
||||
.identity_keys
|
||||
.push(crate::keys::IdentityKey::Nost());
|
||||
key_manager
|
||||
.store_to_keychain(ctx.get_secure_storage().unwrap())
|
||||
.unwrap();
|
||||
|
||||
let db = Database::new(
|
||||
&rust_db_path.display().to_string(),
|
||||
Some(&key_manager.main_key.get_database_key(DatabaseKey::RustDb)),
|
||||
|
|
@ -181,13 +213,9 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
crate::database::tables::received_messages::ReceivedMessage::insert(
|
||||
&db.pool,
|
||||
"sender1",
|
||||
b"original message",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
ReceivedMessage::insert(&db.pool, 1, b"original message")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Add a file
|
||||
let config_file = PathBuf::from(&config.data_dir).join("user_discovery_config.json");
|
||||
|
|
@ -198,6 +226,9 @@ mod tests {
|
|||
let backup_path = BackupArchive::create_backup(&ctx).await.unwrap();
|
||||
assert!(backup_path.exists());
|
||||
|
||||
// Save the original main key bytes
|
||||
let original_main_key = *ctx.get_key_manager().unwrap().main_key.as_bytes();
|
||||
|
||||
// 3. Modify data (to simulate state before restore)
|
||||
{
|
||||
let config = ctx.get_config().unwrap();
|
||||
|
|
@ -211,20 +242,23 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
crate::database::tables::received_messages::ReceivedMessage::insert(
|
||||
&db.pool,
|
||||
"sender2",
|
||||
b"new message",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
ReceivedMessage::insert(&db.pool, 2, 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();
|
||||
|
||||
// Delete old keys to ensure they will be actually restored
|
||||
|
||||
let key_manager = KeyManager::generate().unwrap();
|
||||
key_manager
|
||||
.store_to_keychain(&ctx.get_secure_storage().unwrap())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// 4. Restore backup
|
||||
BackupArchive::restore_from_backup(&ctx, &backup_path)
|
||||
BackupArchive::restore_from_backup(&ctx, &original_main_key, &backup_path)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -241,18 +275,22 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
let messages =
|
||||
crate::database::tables::received_messages::ReceivedMessage::get_all(&db.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
let 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].sender_id, 1);
|
||||
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");
|
||||
|
||||
let key_manager = ctx.get_key_manager().unwrap();
|
||||
assert_eq!(key_manager.identity_keys.len(), 1);
|
||||
match &key_manager.identity_keys[0] {
|
||||
crate::keys::IdentityKey::Nost() => {}
|
||||
_ => panic!("Wrong identity key!"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
115
rust/src/backup/backup_password.rs
Normal file
115
rust/src/backup/backup_password.rs
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
use crate::error::{Result, TwonlyError};
|
||||
use crate::keys::KeyManager;
|
||||
use aes_gcm::aead::rand_core::RngCore;
|
||||
use aes_gcm::aead::{Aead, KeyInit, OsRng};
|
||||
use aes_gcm::{Aes256Gcm, Nonce};
|
||||
use scrypt::{scrypt, Params};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
|
||||
pub(crate) struct BackupPasswordKeys {
|
||||
backup_id: [u8; 32],
|
||||
encryption_key: [u8; 32],
|
||||
}
|
||||
|
||||
impl BackupPasswordKeys {
|
||||
pub(crate) fn new(backup_id: [u8; 32], encryption_key: [u8; 32]) -> Self {
|
||||
Self {
|
||||
backup_id,
|
||||
encryption_key,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_password(password: &str, salt: &str) -> Result<Self> {
|
||||
let params = Params::new(17, 8, 1)?;
|
||||
let mut output = [0u8; 64];
|
||||
|
||||
scrypt(password.as_bytes(), salt.as_bytes(), ¶ms, &mut output)?;
|
||||
|
||||
let mut backup_id = [0u8; 32];
|
||||
let mut encryption_key = [0u8; 32];
|
||||
backup_id.copy_from_slice(&output[0..32]);
|
||||
encryption_key.copy_from_slice(&output[32..64]);
|
||||
|
||||
Ok(Self::new(backup_id, encryption_key))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
|
||||
pub(crate) struct BackupPlainTextContent {
|
||||
pub(crate) user_id: i64,
|
||||
pub(crate) key_manager: KeyManager,
|
||||
}
|
||||
|
||||
impl BackupPlainTextContent {
|
||||
fn get_encrypted_backup(&self) -> Result<Vec<u8>> {
|
||||
let Some(keys) = &self.key_manager.backup_password else {
|
||||
return Err(TwonlyError::Generic("No backup password".into()));
|
||||
};
|
||||
|
||||
let serialized_bytes = postcard::to_allocvec(&self)?;
|
||||
|
||||
let key = aes_gcm::Key::<Aes256Gcm>::from_slice(&keys.encryption_key);
|
||||
let cipher = Aes256Gcm::new(key);
|
||||
|
||||
let mut nonce_bytes = [0u8; 12];
|
||||
OsRng.fill_bytes(&mut nonce_bytes);
|
||||
let nonce = Nonce::from_slice(&nonce_bytes);
|
||||
|
||||
let ciphertext = cipher.encrypt(nonce, serialized_bytes.as_slice())?;
|
||||
|
||||
let mut encrypted_bytes = vec![];
|
||||
encrypted_bytes.extend_from_slice(&nonce_bytes);
|
||||
encrypted_bytes.extend_from_slice(&ciphertext);
|
||||
|
||||
Ok(encrypted_bytes)
|
||||
}
|
||||
|
||||
pub(crate) fn from_encrypted_backup(
|
||||
encrypted_bytes: &[u8],
|
||||
keys: &BackupPasswordKeys,
|
||||
) -> Result<Self> {
|
||||
if encrypted_bytes.len() < 12 {
|
||||
return Err(TwonlyError::Generic(
|
||||
"Invalid encrypted backup length".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let (nonce_bytes, ciphertext) = encrypted_bytes.split_at(12);
|
||||
let nonce = Nonce::from_slice(nonce_bytes);
|
||||
|
||||
let key = aes_gcm::Key::<Aes256Gcm>::from_slice(&keys.encryption_key);
|
||||
let cipher = Aes256Gcm::new(key);
|
||||
|
||||
let decrypted_bytes = cipher.decrypt(nonce, ciphertext)?;
|
||||
|
||||
Ok(postcard::from_bytes(&decrypted_bytes)?)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_backup_encryption_decryption() {
|
||||
let mut key_manager = KeyManager::generate().unwrap();
|
||||
let password = "my_secure_password";
|
||||
let salt = "my_random_salt";
|
||||
|
||||
let keys = BackupPasswordKeys::from_password(password, salt).unwrap();
|
||||
key_manager.backup_password = Some(keys.clone());
|
||||
|
||||
let content = BackupPlainTextContent {
|
||||
user_id: 12345,
|
||||
key_manager,
|
||||
};
|
||||
|
||||
let encrypted = content.get_encrypted_backup().unwrap();
|
||||
let decrypted = BackupPlainTextContent::from_encrypted_backup(&encrypted, &keys).unwrap();
|
||||
|
||||
assert_eq!(content.user_id, decrypted.user_id);
|
||||
assert_eq!(content.key_manager.main_key, decrypted.key_manager.main_key);
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +1,3 @@
|
|||
mod backup_archive;
|
||||
pub(crate) mod backup_password;
|
||||
mod backup_passwordless;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
pub mod callbacks;
|
||||
pub mod wrapper;
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::bridge::callbacks::user_discovery::{
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ impl Context {
|
|||
Err(err) => {
|
||||
tracing::error!("{err}");
|
||||
if rust_db_path.exists() {
|
||||
tracing::error!("Rust Database exsist, while the key manager not");
|
||||
tracing::error!("Rust Database exsist, while the key manager not. This must be a secure storage error.");
|
||||
return Err(TwonlyError::SecureStorageError);
|
||||
}
|
||||
tracing::info!("Generating a new key manager.");
|
||||
|
|
|
|||
|
|
@ -2,15 +2,7 @@
|
|||
|
||||
CREATE TABLE IF NOT EXISTS received_messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sender_id TEXT NOT NULL,
|
||||
sender_id BIGINT 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'
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ impl Database {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::database::tables::received_messages::ReceivedMessage;
|
||||
use chrono::Utc;
|
||||
|
||||
use super::*;
|
||||
use tempfile::tempdir;
|
||||
|
|
@ -109,7 +110,7 @@ mod tests {
|
|||
|
||||
// 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")
|
||||
ReceivedMessage::insert(&db.pool, 1, b"hello world")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -124,7 +125,7 @@ mod tests {
|
|||
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].sender_id, 1);
|
||||
assert_eq!(messages[0].content, b"hello world");
|
||||
}
|
||||
|
||||
|
|
@ -137,7 +138,7 @@ mod tests {
|
|||
let key = "secure_password";
|
||||
|
||||
let db = Database::new(&db_path, Some(key), false).await.unwrap();
|
||||
ReceivedMessage::insert(&db.pool, "sender1", b"hello world")
|
||||
ReceivedMessage::insert(&db.pool, 1, b"hello world")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -154,7 +155,7 @@ mod tests {
|
|||
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");
|
||||
assert_eq!(messages[0].sender_id, 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -165,7 +166,7 @@ mod tests {
|
|||
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")
|
||||
ReceivedMessage::insert(&db.pool, 1, b"hello world")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -175,6 +176,6 @@ mod tests {
|
|||
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");
|
||||
assert_eq!(messages[0].sender_id, 1);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,102 @@
|
|||
pub mod received_messages;
|
||||
pub mod sending_messages;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! generate_insert {
|
||||
($table:literal, $fn_name:ident, $($field:ident : $ty:ty),+) => {
|
||||
pub async fn $fn_name(
|
||||
pool: &sqlx::SqlitePool,
|
||||
$($field: $ty),+
|
||||
) -> crate::error::Result<i64> {
|
||||
let sql = format!(
|
||||
"INSERT INTO {} ({}) VALUES ({}) RETURNING id",
|
||||
$table,
|
||||
vec![$(stringify!($field)),+].join(", "),
|
||||
vec!["?"; [$({stringify!($field); 1}),+].len()].join(", ")
|
||||
);
|
||||
|
||||
let row: (i64,) = sqlx::query_as(sqlx::AssertSqlSafe(sql))
|
||||
$(.bind($field))+
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
Ok(row.0)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! generate_select {
|
||||
($table:literal, $fn_name:ident) => {
|
||||
pub async fn $fn_name(pool: &sqlx::SqlitePool) -> crate::error::Result<Vec<Self>> {
|
||||
let sql = format!("SELECT * FROM {}", $table);
|
||||
let results = sqlx::query_as::<_, Self>(sqlx::AssertSqlSafe(sql))
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
Ok(results)
|
||||
}
|
||||
};
|
||||
($table:literal, $fn_name:ident, $($field:ident : $ty:ty),+) => {
|
||||
pub async fn $fn_name(pool: &sqlx::SqlitePool, $($field: $ty),+) -> crate::error::Result<Vec<Self>> {
|
||||
let mut sql = format!("SELECT * FROM {} WHERE ", $table);
|
||||
let mut filters = Vec::new();
|
||||
$(
|
||||
filters.push(format!("{} = ?", stringify!($field)));
|
||||
)+
|
||||
sql.push_str(&filters.join(" AND "));
|
||||
|
||||
let results = sqlx::query_as::<_, Self>(sqlx::AssertSqlSafe(sql))
|
||||
$(.bind($field))+
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
Ok(results)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! generate_table_tests {
|
||||
(
|
||||
$struct:ident,
|
||||
$insert_fn:ident ($($arg:expr),+),
|
||||
$select_all_fn:ident
|
||||
) => {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::database::Database;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_generated_basic() {
|
||||
let dir = tempdir().unwrap();
|
||||
let db_path = dir.path().join("test.sqlite").display().to_string();
|
||||
let db = Database::new(&db_path, None, false).await.unwrap();
|
||||
|
||||
let _id = $struct::$insert_fn(&db.pool, $($arg),+).await.unwrap();
|
||||
let all = $struct::$select_all_fn(&db.pool).await.unwrap();
|
||||
assert_eq!(all.len(), 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! generate_test_select {
|
||||
($struct:ident, $insert_fn:ident ($($arg:expr),+), $select_fn:ident ($($sel_arg:expr),+)) => {
|
||||
paste::paste! {
|
||||
#[cfg(test)]
|
||||
#[tokio::test]
|
||||
async fn [<test_ $select_fn>]() {
|
||||
use crate::database::Database;
|
||||
use tempfile::tempdir;
|
||||
let dir = tempdir().unwrap();
|
||||
let db_path = dir.path().join("test.sqlite").display().to_string();
|
||||
let db = Database::new(&db_path, None, false).await.unwrap();
|
||||
|
||||
$struct::$insert_fn(&db.pool, $($arg),+).await.unwrap();
|
||||
let results = $struct::$select_fn(&db.pool, $($sel_arg),+).await.unwrap();
|
||||
assert_eq!(results.len(), 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,34 +1,25 @@
|
|||
use crate::error::Result;
|
||||
use chrono::{DateTime, Utc};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
use sqlx::FromRow;
|
||||
|
||||
#[derive(Debug, FromRow)]
|
||||
#[derive(Debug, FromRow, PartialEq, Clone)]
|
||||
pub struct ReceivedMessage {
|
||||
pub id: i64,
|
||||
pub sender_id: String,
|
||||
pub sender_id: i64,
|
||||
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)
|
||||
}
|
||||
crate::generate_insert!(
|
||||
"received_messages",
|
||||
insert,
|
||||
sender_id: i64,
|
||||
content: &[u8]
|
||||
);
|
||||
crate::generate_select!("received_messages", get_all);
|
||||
crate::generate_select!("received_messages", get_by_sender, sender_id: i64);
|
||||
}
|
||||
|
||||
crate::generate_table_tests!(ReceivedMessage, insert(1, b"hello world"), get_all);
|
||||
|
||||
crate::generate_test_select!(ReceivedMessage, insert(1, b"hello world"), get_by_sender(1));
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
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,4 +1,6 @@
|
|||
use hex::FromHexError;
|
||||
use protocols::user_discovery::error::UserDiscoveryError;
|
||||
use scrypt::errors::{InvalidOutputLen, InvalidParams};
|
||||
use thiserror::Error;
|
||||
use zip::result::ZipError;
|
||||
|
||||
|
|
@ -8,24 +10,36 @@ pub type Result<T> = core::result::Result<T, TwonlyError>;
|
|||
pub enum TwonlyError {
|
||||
#[error("global twonly is not initialized")]
|
||||
Initialization,
|
||||
|
||||
#[error("Tried to access the wrong context")]
|
||||
WrongContext,
|
||||
|
||||
#[error("init_flutter_callbacks was not called")]
|
||||
MissingCallbackInitialization,
|
||||
|
||||
#[error("Could not find the given database")]
|
||||
DatabaseNotFound,
|
||||
|
||||
#[error("main_key could not be loaded from the key_chain")]
|
||||
MissingMainKey,
|
||||
|
||||
#[error("{0}")]
|
||||
UserDiscoveryError(#[from] UserDiscoveryError),
|
||||
|
||||
#[error("Error in dart callback")]
|
||||
DartError,
|
||||
|
||||
#[error(
|
||||
"Storage error: database exists but master key could not be loaded from secure storage"
|
||||
)]
|
||||
SecureStorageError,
|
||||
|
||||
#[error("{0}")]
|
||||
SqliteError(#[from] sqlx::Error),
|
||||
|
||||
#[error("{0}")]
|
||||
Generic(String),
|
||||
|
||||
#[error("{0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
|
|
@ -34,6 +48,19 @@ pub enum TwonlyError {
|
|||
|
||||
#[error("{0}")]
|
||||
Walkdir(#[from] walkdir::Error),
|
||||
|
||||
#[error("{0}")]
|
||||
Postcard(#[from] postcard::Error),
|
||||
|
||||
#[error("{0}")]
|
||||
HexError(#[from] FromHexError),
|
||||
|
||||
#[error("{0}")]
|
||||
InvalidParams(#[from] InvalidParams),
|
||||
#[error("{0}")]
|
||||
InvalidOutputLen(#[from] InvalidOutputLen),
|
||||
#[error("AES-GCM error")]
|
||||
AesGcm,
|
||||
}
|
||||
|
||||
impl From<String> for TwonlyError {
|
||||
|
|
@ -47,3 +74,9 @@ impl From<TwonlyError> for UserDiscoveryError {
|
|||
UserDiscoveryError::Store(error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<aes_gcm::Error> for TwonlyError {
|
||||
fn from(_: aes_gcm::Error) -> Self {
|
||||
TwonlyError::AesGcm
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
|
||||
#[derive(Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
|
||||
#[derive(Debug, PartialEq, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
|
||||
pub(crate) enum IdentityKey {
|
||||
Nost(),
|
||||
Signal(),
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ 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)]
|
||||
#[derive(Debug, PartialEq, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
|
||||
pub struct MainKey {
|
||||
/// The 32-byte main master key
|
||||
main_key: [u8; 32],
|
||||
|
|
@ -33,6 +33,22 @@ impl MainKey {
|
|||
Self { main_key }
|
||||
}
|
||||
|
||||
pub fn as_bytes(&self) -> &[u8; 32] {
|
||||
&self.main_key
|
||||
}
|
||||
|
||||
/// Download token required to download a backup.
|
||||
/// This ensures that the user who tries to download the backup must have knowledge over the
|
||||
/// main key
|
||||
pub fn backup_download_token(&self) -> [u8; 32] {
|
||||
self.derive_key(b"backup_download_token")
|
||||
}
|
||||
|
||||
/// Uses as a password to authenitcate agains the server
|
||||
pub fn server_auth_token(&self) -> [u8; 32] {
|
||||
self.derive_key(b"server_auth_token")
|
||||
}
|
||||
|
||||
/// Derives the database encryption key.
|
||||
pub(crate) fn get_database_key(&self, db: DatabaseKey) -> String {
|
||||
let db_name = match db {
|
||||
|
|
@ -43,11 +59,6 @@ impl MainKey {
|
|||
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> {
|
||||
|
|
@ -126,24 +137,6 @@ mod tests {
|
|||
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();
|
||||
|
|
|
|||
|
|
@ -1,20 +1,22 @@
|
|||
mod backup_password;
|
||||
mod backup_passwordless;
|
||||
mod identity_key;
|
||||
mod main_key;
|
||||
|
||||
use crate::backup::backup_password::BackupPasswordKeys;
|
||||
use crate::error::Result;
|
||||
use crate::error::TwonlyError;
|
||||
pub(crate) use crate::keys::identity_key::IdentityKey;
|
||||
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)]
|
||||
#[derive(Debug, PartialEq, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
|
||||
pub(crate) struct KeyManager {
|
||||
pub(crate) main_key: MainKey,
|
||||
pub(crate) identity_keys: Vec<IdentityKey>,
|
||||
pub(crate) backup_password: Option<BackupPasswordKeys>,
|
||||
}
|
||||
|
||||
impl KeyManager {
|
||||
|
|
@ -22,6 +24,7 @@ impl KeyManager {
|
|||
Ok(KeyManager {
|
||||
main_key: MainKey::generate(),
|
||||
identity_keys: vec![],
|
||||
backup_password: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -29,20 +32,18 @@ impl KeyManager {
|
|||
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())?;
|
||||
.ok_or_else(|| TwonlyError::MissingMainKey)?;
|
||||
|
||||
let bytes = hex::decode(hex_key).map_err(|e| format!("Failed to decode hex key: {}", e))?;
|
||||
let bytes = hex::decode(hex_key)?;
|
||||
|
||||
let main_key: KeyManager = postcard::from_bytes(&bytes)
|
||||
.map_err(|e| format!("Failed to deserialize KeyManager: {}", e))?;
|
||||
let main_key: KeyManager = postcard::from_bytes(&bytes)?;
|
||||
|
||||
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 serialized = postcard::to_allocvec(self)?;
|
||||
|
||||
let hex_key = hex::encode(serialized);
|
||||
storage.write(KEY_MANAGER_ID, &hex_key)?;
|
||||
|
|
|
|||
|
|
@ -32,9 +32,13 @@ impl SecureStorage {
|
|||
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
use std::collections::HashMap;
|
||||
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))?;
|
||||
let mut config = HashMap::new();
|
||||
config.insert("access-group", group);
|
||||
let store =
|
||||
apple_native_keyring_store::protected::Store::new_with_configuration(&config)
|
||||
.map_err(|e| format!("Failed to init iOS Protected Store: {}", e))?;
|
||||
keyring_core::set_default_store(store);
|
||||
}
|
||||
|
||||
|
|
@ -62,8 +66,7 @@ impl SecureStorage {
|
|||
/// * `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))?;
|
||||
let entry = self.get_entry(key)?;
|
||||
|
||||
entry
|
||||
.set_password(value)
|
||||
|
|
@ -77,8 +80,7 @@ impl SecureStorage {
|
|||
/// 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))?;
|
||||
let entry = self.get_entry(key)?;
|
||||
|
||||
match entry.get_password() {
|
||||
Ok(password) => Ok(Some(password)),
|
||||
|
|
@ -91,8 +93,7 @@ impl SecureStorage {
|
|||
///
|
||||
/// 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))?;
|
||||
let entry = self.get_entry(key)?;
|
||||
|
||||
match entry.delete_credential() {
|
||||
Ok(()) => Ok(()),
|
||||
|
|
@ -100,6 +101,24 @@ impl SecureStorage {
|
|||
Err(e) => Err(format!("Failed to delete secret from keyring: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to create a keyring entry with the appropriate platform modifiers.
|
||||
fn get_entry(&self, key: &str) -> Result<Entry, String> {
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
use std::collections::HashMap;
|
||||
let mut modifiers = HashMap::new();
|
||||
modifiers.insert("access-policy", "AfterFirstUnlock");
|
||||
Entry::new_with_modifiers(&self.service_name, key, &modifiers)
|
||||
.map_err(|e| format!("Failed to create keyring entry with modifiers: {}", e))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
{
|
||||
Entry::new(&self.service_name, key)
|
||||
.map_err(|e| format!("Failed to create keyring entry: {}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
Loading…
Reference in a new issue