Skip to content

Commit 8e05a75

Browse files
committed
Allow self-signed certificates
1 parent 73615a6 commit 8e05a75

9 files changed

+111
-42
lines changed

lib/api.dart

+28
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import 'dart:async';
22
import 'dart:convert';
3+
import 'dart:io';
34

5+
import 'package:crypto/crypto.dart';
46
import 'package:dio/dio.dart';
57
import 'package:flutter_secure_storage/flutter_secure_storage.dart'
68
as SecureStorage;
@@ -204,6 +206,7 @@ class API {
204206
String? authString;
205207
String apiFlavour;
206208
final Dio dio = new Dio();
209+
static String? trustedCertificateSha512;
207210

208211
API(String baseURL,
209212
{this.username = "", this.password = "", this.apiFlavour = "paperless"}) {
@@ -399,3 +402,28 @@ class API {
399402
await updateResource("document", id, newDocument);
400403
}
401404
}
405+
406+
class SelfSignedCertHttpOverride extends HttpOverrides {
407+
static X509Certificate? lastFailedCert;
408+
static String toSha512(X509Certificate cert) {
409+
return sha512.convert(cert.der).toString();
410+
}
411+
412+
@override
413+
HttpClient createHttpClient(SecurityContext? context) {
414+
return super.createHttpClient(context)
415+
..badCertificateCallback = (X509Certificate cert, String host, int port) {
416+
if (!cert.endValidity.isAfter(DateTime.now())) {
417+
// Never trust an expired certificate
418+
lastFailedCert = cert;
419+
return false;
420+
}
421+
if (API.trustedCertificateSha512 != toSha512(cert)) {
422+
// Abort if this is not the known certificate
423+
lastFailedCert = cert;
424+
return false;
425+
}
426+
return true;
427+
};
428+
}
429+
}

lib/main.dart

+4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import 'dart:io';
2+
13
import 'package:flutter/material.dart';
24
import 'package:flutter_localizations/flutter_localizations.dart';
35
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
46
import 'package:flutter_svg/flutter_svg.dart';
57
import 'package:get_it/get_it.dart';
68
import 'package:i18n_extension/i18n_widget.dart';
9+
import 'package:paperless_app/api.dart';
710
import 'package:paperless_app/i18n.dart';
811
import 'package:paperless_app/routes/home_route.dart';
912

@@ -82,5 +85,6 @@ class _PaperlessAppState extends State<PaperlessApp> {
8285
super.initState();
8386
GetIt.I.registerSingleton<FlutterSecureStorage>(new FlutterSecureStorage());
8487
loadAsync = MyI18n.loadTranslations();
88+
HttpOverrides.global = SelfSignedCertHttpOverride();
8589
}
8690
}

lib/routes/documents_route.dart

+4-25
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'dart:async';
22
import 'dart:io';
33

4+
import 'package:dio/dio.dart';
45
import 'package:flutter/material.dart';
56
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
67
import 'package:flutter_svg/flutter_svg.dart';
@@ -26,6 +27,7 @@ import 'package:url_launcher/url_launcher.dart';
2627
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
2728

2829
import '../api.dart';
30+
import '../util/handle_dio_error.dart';
2931

3032
class DocumentsRoute extends StatefulWidget {
3133
static DateFormat dateFormat = DateFormat();
@@ -122,31 +124,8 @@ class _DocumentsRouteState extends State<DocumentsRoute> {
122124
documents = _documents;
123125
requesting = false;
124126
});
125-
} catch (e) {
126-
showDialog(
127-
context: _scaffoldKey.currentContext!,
128-
builder: (BuildContext context) {
129-
return AlertDialog(
130-
title: Text("Error while connecting to server".i18n),
131-
content: Text(e.toString()),
132-
actions: <Widget>[
133-
new TextButton(
134-
onPressed: () {
135-
Navigator.pop(context);
136-
reloadDocuments();
137-
},
138-
child: Text("Retry".i18n)),
139-
new TextButton(
140-
onPressed: () {
141-
Navigator.pushReplacement(
142-
context,
143-
MaterialPageRoute(
144-
builder: (context) => ServerDetailsRoute()),
145-
);
146-
},
147-
child: Text("Edit Server Details".i18n))
148-
]);
149-
});
127+
} on DioError catch (e) {
128+
handleDioError(e, context);
150129
}
151130
}
152131

lib/routes/home_route.dart

+4
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ class _HomeRouteState extends State<HomeRoute> {
5252

5353
var username = await GetIt.I<FlutterSecureStorage>().read(key: "username");
5454
var password = await GetIt.I<FlutterSecureStorage>().read(key: "password");
55+
var trustedCertificateSha512 = await GetIt.I<FlutterSecureStorage>()
56+
.read(key: "trustedCertificateSha512");
5557
var apiFlavour =
5658
await GetIt.I<FlutterSecureStorage>().read(key: "api_flavour");
5759

@@ -66,6 +68,8 @@ class _HomeRouteState extends State<HomeRoute> {
6668
apiFlavour = "paperless";
6769
}
6870

71+
API.trustedCertificateSha512 = trustedCertificateSha512;
72+
6973
API(url, username: username, password: password, apiFlavour: apiFlavour);
7074

7175
Navigator.pushReplacement(

lib/routes/login_route.dart

+4-8
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:dio/dio.dart';
12
import 'package:flutter/material.dart';
23
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
34
import 'package:flutter_svg/flutter_svg.dart';
@@ -9,6 +10,7 @@ import 'package:paperless_app/widgets/display_steps_widget.dart';
910
import 'package:paperless_app/widgets/textfield_widget.dart';
1011

1112
import '../api.dart';
13+
import '../util/handle_dio_error.dart';
1214

1315
final _formKey = GlobalKey<FormState>();
1416
final _scaffoldKey = GlobalKey<ScaffoldState>();
@@ -48,14 +50,8 @@ class _LoginRouteState extends State<LoginRoute> {
4850
MaterialPageRoute(builder: (context) => DocumentsRoute()),
4951
);
5052
}
51-
} catch (e) {
52-
showDialog(
53-
context: _scaffoldKey.currentContext!,
54-
builder: (BuildContext ctx) {
55-
return AlertDialog(
56-
title: Text("Error while connecting to server".i18n),
57-
content: Text(e.toString()));
58-
});
53+
} on DioError catch (e) {
54+
handleDioError(e, context);
5955
}
6056
}
6157
}

lib/routes/server_details_route.dart

+4-8
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:dio/dio.dart';
12
import 'package:flutter/material.dart';
23
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
34
import 'package:flutter_svg/flutter_svg.dart';
@@ -7,6 +8,7 @@ import 'package:paperless_app/i18n.dart';
78
import 'package:paperless_app/widgets/button_widget.dart';
89
import 'package:paperless_app/widgets/textfield_widget.dart';
910

11+
import '../util/handle_dio_error.dart';
1012
import '../widgets/display_steps_widget.dart';
1113
import 'login_route.dart';
1214

@@ -46,14 +48,8 @@ class _ServerDetailsRouteState extends State<ServerDetailsRoute> {
4648
MaterialPageRoute(builder: (context) => LoginRoute()),
4749
);
4850
}
49-
} catch (e) {
50-
showDialog(
51-
context: _scaffoldKey.currentContext!,
52-
builder: (BuildContext ctx) {
53-
return AlertDialog(
54-
title: Text("Error while connecting to server".i18n),
55-
content: Text(e.toString()));
56-
});
51+
} on DioError catch (e) {
52+
handleDioError(e, _scaffoldKey.currentContext!);
5753
}
5854
}
5955
}

lib/util/handle_dio_error.dart

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import 'dart:io';
2+
3+
import 'package:convert/convert.dart';
4+
import 'package:dio/dio.dart';
5+
import 'package:flutter/material.dart';
6+
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
7+
import 'package:get_it/get_it.dart';
8+
import 'package:paperless_app/i18n.dart';
9+
10+
import '../api.dart';
11+
12+
void handleDioError(DioError e, BuildContext ctx) {
13+
if (e.error.runtimeType == HandshakeException &&
14+
SelfSignedCertHttpOverride.lastFailedCert != null) {
15+
String fingerprint =
16+
hex.encode(SelfSignedCertHttpOverride.lastFailedCert!.sha1);
17+
showDialog(
18+
context: ctx,
19+
builder: (BuildContext ctx) {
20+
return AlertDialog(
21+
backgroundColor: Colors.red.shade900,
22+
title: Text("This is not a secure connection".i18n),
23+
content: Text(
24+
"The certificate for %s is not valid. Do you still want to trust the certificate with fingerprint %s?"
25+
.i18n
26+
.fill([e.requestOptions.uri.host, fingerprint]) +
27+
"\n" +
28+
"WARNING: The connection is not secure if you do not compare the fingerprint."
29+
.i18n),
30+
actions: [
31+
TextButton(
32+
onPressed: () {
33+
API.trustedCertificateSha512 =
34+
SelfSignedCertHttpOverride.toSha512(
35+
SelfSignedCertHttpOverride.lastFailedCert!);
36+
GetIt.I<FlutterSecureStorage>().write(
37+
key: "trustedCertificateSha512",
38+
value: API.trustedCertificateSha512);
39+
Navigator.of(ctx).pop();
40+
},
41+
child: Text('Yes'.i18n),
42+
),
43+
TextButton(
44+
onPressed: () {
45+
// Close the dialog
46+
Navigator.of(ctx).pop();
47+
},
48+
child: Text('No'.i18n),
49+
),
50+
],
51+
);
52+
});
53+
} else
54+
showDialog(
55+
context: ctx,
56+
builder: (BuildContext ctx) {
57+
return AlertDialog(
58+
title: Text("Error while connecting to server".i18n),
59+
content: Text(e.toString()));
60+
});
61+
}

pubspec.lock

+1-1
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ packages:
177177
source: hosted
178178
version: "3.0.1"
179179
crypto:
180-
dependency: transitive
180+
dependency: "direct main"
181181
description:
182182
name: crypto
183183
url: "https://pub.dartlang.org"

pubspec.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ dependencies:
4141
wechat_assets_picker: ^5.4.0
4242
share: ^2.0.1
4343
url_launcher: ^6.0.20
44+
crypto: ^3.0.1
4445

4546
dev_dependencies:
4647
flutter_test:

0 commit comments

Comments
 (0)