Skip to content

Commit 1daa024

Browse files
committed
added support for encryption (with pgp)
1 parent 1be53d7 commit 1daa024

File tree

7 files changed

+479
-31
lines changed

7 files changed

+479
-31
lines changed

lib/services/database.dart

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ class Share extends HiveObject {
3333
@HiveField(9)
3434
String? method;
3535

36+
@HiveField(10)
37+
String? pgpPublicKey;
38+
3639
Share({
3740
required this.uploaderUrl,
3841
required this.formDataName,
@@ -44,6 +47,7 @@ class Share extends HiveObject {
4447
required this.uploaderErrorParser,
4548
this.selectedUploader = false, // Default to false
4649
this.method,
50+
this.pgpPublicKey,
4751
});
4852

4953
// Setter method to safely update selectedUploader
@@ -89,6 +93,9 @@ class NetworkShare extends HiveObject {
8993
@HiveField(7)
9094
String? urlPath;
9195

96+
@HiveField(8)
97+
String? pgpPublicKey;
98+
9299
NetworkShare({
93100
required this.protocol,
94101
required this.domain,
@@ -98,9 +105,10 @@ class NetworkShare extends HiveObject {
98105
this.port = 21, // can set default
99106
this.selected = false,
100107
this.urlPath = "",
108+
this.pgpPublicKey,
101109
});
102110

103111
void setSelectedShare(bool value) {
104112
selected = value;
105113
}
106-
}
114+
}

lib/services/database.g.dart

Lines changed: 10 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/services/file_upload.dart

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'dart:io';
22
import 'dart:math';
3+
import 'package:custom_uploader/services/pgp_service.dart';
34
import 'package:custom_uploader/utils/response_parser.dart';
45
import 'package:custom_uploader/utils/show_message.dart';
56
import 'package:custom_uploader/services/response_logger.dart';
@@ -124,7 +125,7 @@ class FileService {
124125
if(uploader != null) {
125126
try {
126127
String mimeType = lookupMimeType(file.path) ?? 'application/octet-stream';
127-
FormData formData = _buildFormData(uploader, file, mimeType);
128+
FormData formData = await _buildFormData(uploader, file, mimeType);
128129
Map<String, String> headers = await _getHeaders(uploader);
129130

130131
return await _uploadFile(
@@ -150,13 +151,25 @@ class FileService {
150151
await client.connect();
151152

152153
final remotePath = networkUploader.folderPath.isEmpty ? "/" : networkUploader.folderPath;
153-
final ftpFile = FtpFile(path: remotePath.endsWith("/") ? remotePath + file.path.split("/").last : "$remotePath/${file.path.split("/").last}", client: client);
154+
final remoteFilename = networkUploader.pgpPublicKey != null
155+
? "${file.path.split("/").last}.pgp"
156+
: file.path.split("/").last;
157+
final ftpFile = FtpFile(path: remotePath.endsWith("/") ? remotePath + remoteFilename : "$remotePath/$remoteFilename}", client: client);
158+
159+
final encryptedStream = encryptPgpStream(
160+
file.openRead(),
161+
networkUploader.pgpPublicKey,
162+
);
163+
164+
final encryptedSize = networkUploader.pgpPublicKey != null
165+
? null // unknown after encryption
166+
: await file.length();
154167

155168
final result = await uploadFtpFile(
156169
ftpClient: client,
157170
targetFile: ftpFile,
158-
fileData: file.openRead(),
159-
fileSize: await file.length(),
171+
fileData: encryptedStream,
172+
fileSize: encryptedSize ?? 0,
160173
onUploadProgress: (sent, total, percent) {
161174
setOnUploadProgress(sent, total);
162175
},
@@ -220,14 +233,28 @@ class FileService {
220233
};
221234
}
222235

236+
static Future<FormData> _buildFormData(Share uploader, File file, String mimeType) async {
237+
Uint8List bytes = await file.readAsBytes();
238+
bytes = await encryptPgpBytes(bytes, uploader.pgpPublicKey);
239+
240+
final filename = uploader.pgpPublicKey != null
241+
? "${file.path.split("/").last}.asc"
242+
: file.path.split("/").last;
243+
244+
MultipartFile filePart = MultipartFile.fromBytes(
245+
bytes,
246+
filename: filename,
247+
contentType: MediaType.parse(
248+
uploader.pgpPublicKey != null
249+
? 'application/pgp-encrypted'
250+
: mimeType,
251+
),
252+
);
223253

224-
static FormData _buildFormData(Share uploader, File file, String mimeType) {
225-
MultipartFile filePart = uploader.uploadFormData ?
226-
MultipartFile.fromBytes(file.readAsBytesSync(), contentType: MediaType.parse(mimeType),
227-
) :
228-
MultipartFile.fromFileSync(file.path, filename: file.path.split("/").last, contentType: MediaType.parse(mimeType));
254+
FormData formData = FormData.fromMap({
255+
uploader.formDataName: filePart,
256+
});
229257

230-
FormData formData = FormData.fromMap({uploader.formDataName: filePart});
231258
uploader.uploadArguments.forEach((key, value) {
232259
formData.fields.add(MapEntry(key, value));
233260
});

lib/services/pgp_service.dart

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import 'dart:convert';
2+
import 'dart:io';
3+
import 'dart:typed_data';
4+
5+
import 'package:file_picker/file_picker.dart';
6+
import 'package:flutter/material.dart';
7+
import 'package:dart_pg/dart_pg.dart';
8+
9+
import '../utils/show_message.dart';
10+
11+
Future<String?> importPgpKeyFromFile(FilePickerResult? result, BuildContext context) async {
12+
if (result == null || result.files.single.path == null) return null;
13+
14+
final content = await File(result.files.single.path!).readAsString();
15+
final isValid = await _cryptoValidateKey(content);
16+
if(isValid) {
17+
return content.trim();
18+
} else {
19+
showSnackBar(
20+
context,
21+
'Invalid PGP key',
22+
);
23+
return null;
24+
}
25+
}
26+
27+
Future<bool> _cryptoValidateKey(String key) async {
28+
return key.contains('BEGIN PGP PUBLIC KEY BLOCK');
29+
}
30+
31+
Future<String?> generateNewPgpKey(BuildContext context, String uploaderName) async {
32+
final nameCtl = TextEditingController();
33+
final emailCtl = TextEditingController();
34+
final passCtl = TextEditingController();
35+
36+
final confirmed = await showDialog(
37+
context: context,
38+
builder: (context) {
39+
return AlertDialog(
40+
title: Text('Generate New PGP Key'),
41+
content: Column(
42+
mainAxisSize: MainAxisSize.min,
43+
children: [
44+
TextField(controller: nameCtl, decoration: InputDecoration(labelText: 'Name')),
45+
TextField(controller: emailCtl, decoration: InputDecoration(labelText: 'Email')),
46+
TextField(controller: passCtl, decoration: InputDecoration(labelText: 'Password' ), obscureText: true),
47+
]
48+
),
49+
actions: [
50+
TextButton(
51+
onPressed: () => Navigator.pop(context, false),
52+
child: Text('Cancel'),
53+
),
54+
TextButton(
55+
onPressed: () => Navigator.pop(context, true),
56+
child: Text('Confirm'),
57+
),
58+
],
59+
);}
60+
);
61+
62+
if(confirmed) {
63+
try {
64+
final userId = [
65+
nameCtl.text,
66+
'<${emailCtl.text}>',
67+
].join(' ');
68+
69+
final privateKey = OpenPGP.generateKey(
70+
[userId],
71+
passCtl.text,
72+
type: KeyType.curve25519,
73+
);
74+
75+
await _offerPrivateKeyExport(context, privateKey.armor(), uploaderName);
76+
77+
return privateKey.publicKey.armor();
78+
} catch (e) {
79+
showSnackBar(context, 'Key generation failed: $e');
80+
return null;
81+
}
82+
}
83+
return null;
84+
}
85+
86+
Future<void> _offerPrivateKeyExport(BuildContext context, String privateKey, String uploaderName) async {
87+
final confirmed = await showDialog(
88+
context: context,
89+
builder: (context) {
90+
return AlertDialog(
91+
title: Text('Export Private Key'),
92+
content: Text('Do you want to export the private key?'),
93+
actions: [
94+
TextButton(
95+
onPressed: () => Navigator.pop(context, false),
96+
child: Text('Cancel'),
97+
),
98+
TextButton(
99+
onPressed: () => Navigator.pop(context, true),
100+
child: Text('Export'),
101+
),
102+
],
103+
);
104+
}
105+
);
106+
if (confirmed) {
107+
final path = await FilePicker.platform.saveFile(fileName: 'private_key-$uploaderName-${DateTime.now()}.asc', bytes: Uint8List.fromList(privateKey.codeUnits));
108+
109+
if (path != null) {
110+
await File(path).writeAsString(privateKey);
111+
}
112+
}
113+
}
114+
115+
116+
Future<Uint8List> encryptPgpBytes(
117+
Uint8List data,
118+
String? armoredPublicKey,
119+
) async {
120+
if (armoredPublicKey == null || armoredPublicKey.isEmpty) {
121+
return data;
122+
}
123+
124+
final publicKey = OpenPGP.readPublicKey(armoredPublicKey);
125+
final encrypted = OpenPGP.encryptBinaryData(data, encryptionKeys: [publicKey]);
126+
final armored = encrypted.armor();
127+
return Uint8List.fromList(utf8.encode(armored));
128+
}
129+
130+
Stream<List<int>> encryptPgpStream(
131+
Stream<List<int>> data,
132+
String? armoredPublicKey,
133+
) async* {
134+
if (armoredPublicKey == null || armoredPublicKey.isEmpty) {
135+
yield* data;
136+
return;
137+
}
138+
139+
final buffer = BytesBuilder();
140+
await for (final chunk in data) {
141+
buffer.add(chunk);
142+
}
143+
144+
final publicKey = OpenPGP.readPublicKey(armoredPublicKey);
145+
final encrypted = OpenPGP.encryptBinaryData(buffer.toBytes(), encryptionKeys: [publicKey]);
146+
final armored = encrypted.armor();
147+
yield* Stream.fromIterable([utf8.encode(armored)]);
148+
return;
149+
}

0 commit comments

Comments
 (0)