Import and Export Profile

This commit is contained in:
Sarah Jamie Lewis 2022-03-09 14:35:12 -08:00
parent 915cf1a6d8
commit 6237032716
9 changed files with 212 additions and 7 deletions

View File

@ -428,6 +428,16 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
notificationConversationInfo = (a.get("notificationConversationInfo") as? String)
?: "New Message From "
}
"ExportProfile" -> {
val profileOnion = (a.get("ProfileOnion") as? String) ?: ""
val file = (a.get("file") as? String) ?: ""
Cwtch.exportProfile(profileOnion, file)
}
"ImportProfile" -> {
val file = (a.get("file") as? String) ?: ""
val password = (a.get("pass") as? String) ?: ""
return Result.success(Data.Builder().putString("result", Cwtch.importProfile(file, pass)).build());
}
else -> {
Log.i(TAG, "unknown command: " + method);
return Result.failure()

View File

@ -0,0 +1,43 @@
import 'package:cwtch/widgets/passwordfield.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void showPasswordDialog(BuildContext context, String title, String action, Function(String) onEntered) {
TextEditingController passwordController = TextEditingController();
CwtchPasswordField passwordField = CwtchPasswordField(
controller: passwordController,
validator: (passsword) {
return null;
});
// set up the buttons
Widget cancelButton = ElevatedButton(
child: Text(AppLocalizations.of(context)!.cancel),
onPressed: () {
Navigator.of(context).pop(); // dismiss dialog
},
);
Widget continueButton = ElevatedButton(
child: Text(action),
onPressed: () {
onEntered(passwordController.value.text);
});
// set up the AlertDialog
AlertDialog alert = AlertDialog(
title: Text(title),
content: passwordField,
actions: [
cancelButton,
continueButton,
],
);
// show the dialog
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
}

View File

@ -28,3 +28,17 @@ void showFilePicker(BuildContext ctx, int maxBytes, Function(File) onSuccess, Fu
onCancel();
}
}
Future<String?> showCreateFilePicker(BuildContext ctx) async {
// only allow one file picker at a time
// note: ideally we would destroy file picker when leaving a conversation
// but we don't currently have that option.
// we need to store AppState in a variable because ctx might be destroyed
// while awaiting for pickFiles.
var appstate = Provider.of<AppState>(ctx, listen: false);
appstate.disableFilePicker = true;
// currently lockParentWindow only works on Windows...
String? result = await FilePicker.platform.saveFile(lockParentWindow: true);
appstate.disableFilePicker = false;
return result;
}

View File

@ -1,5 +1,3 @@
import 'package:flutter/src/services/text_input.dart';
// To handle profiles that are "unencrypted" (i.e don't require a password to open) we currently create a profile with a defacto, hardcoded password.
// Details: https://docs.openprivacy.ca/cwtch-security-handbook/profile_encryption_and_storage.html
const DefaultPassword = "be gay do crime";
@ -19,6 +17,11 @@ abstract class Cwtch {
// ignore: non_constant_identifier_names
void ChangePassword(String profile, String pass, String newpass, String newpassAgain);
// ignore: non_constant_identifier_names
void ExportProfile(String profile, String file);
// ignore: non_constant_identifier_names
Future<dynamic> ImportProfile(String file, String pass);
// ignore: non_constant_identifier_names
void ResetTor();

View File

@ -52,6 +52,9 @@ typedef StringFn = void Function(Pointer<Utf8> dir, int);
typedef string_string_to_void_function = Void Function(Pointer<Utf8> str, Int32 length, Pointer<Utf8> str2, Int32 length2);
typedef StringStringFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int);
typedef string_string_to_string_function = Pointer<Utf8> Function(Pointer<Utf8> str, Int32 length, Pointer<Utf8> str2, Int32 length2);
typedef StringFromStringStringFn = Pointer<Utf8> Function(Pointer<Utf8>, int, Pointer<Utf8>, int);
typedef string_int_to_void_function = Void Function(Pointer<Utf8> str, Int32 length, Int32 handle);
typedef VoidFromStringIntFn = void Function(Pointer<Utf8>, int, int);
@ -756,4 +759,33 @@ class CwtchFfi implements Cwtch {
cwtchNotifier.l10nInit(notificationSimple, notificationConversationInfo);
_isL10nInit = true;
}
@override
// ignore: non_constant_identifier_names
void ExportProfile(String profile, String file) {
final utf8profile = profile.toNativeUtf8();
final utf8file = file.toNativeUtf8();
var exportProfileC = library.lookup<NativeFunction<void_from_string_string_function>>("c_ExportProfile");
// ignore: non_constant_identifier_names
final ExportProfileFn = exportProfileC.asFunction<VoidFromStringStringFn>();
ExportProfileFn(utf8profile, utf8profile.length, utf8file, utf8file.length);
malloc.free(utf8profile);
malloc.free(utf8file);
}
@override
// ignore: non_constant_identifier_names
Future<String> ImportProfile(String file, String pass) async {
final utf8pass = pass.toNativeUtf8();
final utf8file = file.toNativeUtf8();
var exportProfileC = library.lookup<NativeFunction<string_string_to_string_function>>("c_ImportProfile");
// ignore: non_constant_identifier_names
final ExportProfileFn = exportProfileC.asFunction<StringFromStringStringFn>();
Pointer<Utf8> result = ExportProfileFn(utf8file, utf8file.length, utf8pass, utf8pass.length);
String importResult = result.toDartString();
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(result);
malloc.free(utf8pass);
malloc.free(utf8file);
return importResult;
}
}

View File

@ -308,4 +308,16 @@ class CwtchGomobile implements Cwtch {
cwtchPlatform.invokeMethod("L10nInit", {"notificationSimple": notificationSimple, "notificationConversationInfo": notificationConversationInfo});
_isL10nInit = true;
}
@override
// ignore: non_constant_identifier_names
void ExportProfile(String profile, String file) {
cwtchPlatform.invokeMethod("ExportProfile", {"ProfileOnion": profile, "file": file});
}
@override
// ignore: non_constant_identifier_names
Future<dynamic> ImportProfile(String file, String pass) {
return cwtchPlatform.invokeMethod("ImportProfile", {"file": file, "pass": pass});
}
}

View File

@ -1,6 +1,7 @@
import 'dart:io';
import 'package:cwtch/config.dart';
import 'package:cwtch/controllers/filesharing.dart';
import 'package:cwtch/cwtch/cwtch.dart';
import 'package:cwtch/models/appstate.dart';
import 'package:cwtch/models/profile.dart';
@ -286,6 +287,28 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
),
],
),
Visibility(
visible: Provider.of<ProfileInfoState>(context, listen: false).onion.isNotEmpty,
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end, children: [
SizedBox(
height: 20,
),
Tooltip(
message: AppLocalizations.of(context)!.exportProfileTooltip,
child: ElevatedButton.icon(
onPressed: () {
showCreateFilePicker(context).then((name) {
if (name != null) {
Provider.of<FlwtchState>(context, listen: false).cwtch.ExportProfile(ctrlrOnion.value.text, name);
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.fileSavedTo + " " + name));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
});
},
icon: Icon(Icons.import_export),
label: Text(AppLocalizations.of(context)!.exportProfile),
))
])),
Visibility(
visible: Provider.of<ProfileInfoState>(context, listen: false).onion.isNotEmpty,
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end, children: [

View File

@ -1,6 +1,9 @@
import 'dart:convert';
import 'dart:io';
import 'package:cwtch/constants.dart';
import 'package:cwtch/controllers/enter_password.dart';
import 'package:cwtch/controllers/filesharing.dart';
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:cwtch/models/appstate.dart';
import 'package:cwtch/models/profile.dart';
@ -67,7 +70,7 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
actions: getActions(),
),
floatingActionButton: FloatingActionButton(
onPressed: _pushAddProfile,
onPressed: _modalAddImportProfiles,
tooltip: AppLocalizations.of(context)!.addNewProfileBtn,
child: Icon(
Icons.add,
@ -159,8 +162,9 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
));
}
void _pushAddProfile({onion: ""}) {
Navigator.of(context).push(MaterialPageRoute<void>(
void _pushAddProfile(bcontext, {onion: ""}) {
Navigator.popUntil(bcontext, (route) => route.isFirst);
Navigator.of(bcontext).push(MaterialPageRoute<void>(
builder: (BuildContext context) {
return MultiProvider(
providers: [
@ -174,6 +178,70 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
));
}
void _modalAddImportProfiles() {
showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
builder: (BuildContext context) {
return Padding(
padding: MediaQuery.of(context).viewInsets,
child: RepaintBoundary(
child: Container(
height: 200, // bespoke value courtesy of the [TextField] docs
child: Center(
child: Padding(
padding: EdgeInsets.all(10.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
Spacer(),
Expanded(
child: ElevatedButton(
child: Text(AppLocalizations.of(context)!.addProfileTitle, semanticsLabel: AppLocalizations.of(context)!.addProfileTitle),
onPressed: () {
_pushAddProfile(context);
},
)),
Spacer()
]),
SizedBox(
height: 20,
),
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
Spacer(),
Expanded(
child: Tooltip(
message: AppLocalizations.of(context)!.importProfileTooltip,
child: ElevatedButton(
child: Text(AppLocalizations.of(context)!.importProfile, semanticsLabel: AppLocalizations.of(context)!.importProfile),
onPressed: () {
// 10GB profiles should be enough for anyone?
showFilePicker(context, MaxGeneralFileSharingSize, (file) {
showPasswordDialog(context, AppLocalizations.of(context)!.importProfile, AppLocalizations.of(context)!.importProfile, (password) {
Navigator.popUntil(context, (route) => route.isFirst);
Provider.of<FlwtchState>(context, listen: false).cwtch.ImportProfile(file.path, password).then((value) {
if (value == "") {
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.successfullyImportedProfile.replaceFirst("%profile", file.path)));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
} else {
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.failedToImportProfile));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
});
});
}, () {}, () {});
},
))),
Spacer()
]),
],
))),
)));
});
}
void _modalUnlockProfiles() {
showModalBottomSheet<void>(
context: context,

View File

@ -46,10 +46,10 @@ class FileBubbleState extends State<FileBubble> {
@override
Widget build(BuildContext context) {
var fromMe = Provider.of<MessageMetadata>(context, listen: false).senderHandle == Provider.of<ProfileInfoState>(context).onion;
var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion;
var flagStarted = Provider.of<MessageMetadata>(context).attributes["file-downloaded"] == "true";
var borderRadiousEh = 15.0;
var showFileSharing = Provider.of<Settings>(context, listen: false).isExperimentEnabled(FileSharingExperiment);
var showFileSharing = Provider.of<Settings>(context).isExperimentEnabled(FileSharingExperiment);
DateTime messageDate = Provider.of<MessageMetadata>(context).timestamp;
var metadata = Provider.of<MessageMetadata>(context);