2024-06-19 15:07:18 +02:00
|
|
|
import 'dart:async';
|
2021-05-09 12:31:27 +02:00
|
|
|
import 'dart:convert';
|
|
|
|
import 'dart:io';
|
|
|
|
import 'dart:typed_data';
|
|
|
|
|
2024-06-19 15:07:18 +02:00
|
|
|
import 'package:equatable/equatable.dart';
|
2021-05-09 12:31:27 +02:00
|
|
|
import 'package:logging/logging.dart';
|
|
|
|
import 'package:nc_photos/mobile/android/self_signed_cert.dart';
|
2022-12-16 16:01:04 +01:00
|
|
|
import 'package:np_codegen/np_codegen.dart';
|
2023-02-23 15:49:17 +01:00
|
|
|
import 'package:np_common/type.dart';
|
2024-06-19 15:07:18 +02:00
|
|
|
import 'package:np_string/np_string.dart';
|
2022-01-31 10:47:37 +01:00
|
|
|
import 'package:path/path.dart' as path_lib;
|
2021-05-09 12:31:27 +02:00
|
|
|
import 'package:path_provider/path_provider.dart';
|
|
|
|
import 'package:uuid/uuid.dart';
|
|
|
|
|
2022-12-16 16:01:04 +01:00
|
|
|
part 'self_signed_cert_manager.g.dart';
|
|
|
|
|
|
|
|
@npLog
|
2021-05-09 12:31:27 +02:00
|
|
|
class SelfSignedCertManager {
|
|
|
|
factory SelfSignedCertManager() => _inst;
|
|
|
|
|
2024-06-19 10:17:24 +02:00
|
|
|
SelfSignedCertManager._();
|
2021-05-09 12:31:27 +02:00
|
|
|
|
2024-06-19 10:17:24 +02:00
|
|
|
Future<void> init() async {
|
2021-05-09 12:31:27 +02:00
|
|
|
HttpOverrides.global = _CustomHttpOverrides();
|
2024-06-19 10:17:24 +02:00
|
|
|
final infos = await _readAllCerts();
|
|
|
|
_whitelist = infos;
|
2021-05-09 12:31:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Verify [cert] and return if it's registered in the whitelist for [host]
|
|
|
|
bool verify(X509Certificate cert, String host, int port) {
|
|
|
|
final fingerprint = _sha1BytesToString(cert.sha1);
|
|
|
|
return _whitelist.any((info) =>
|
|
|
|
fingerprint == info.sha1 &&
|
|
|
|
host.toLowerCase() == info.host.toLowerCase());
|
|
|
|
}
|
|
|
|
|
|
|
|
String getLastBadCertHost() => _latestBadCert.host;
|
|
|
|
|
|
|
|
String getLastBadCertFingerprint() =>
|
|
|
|
_sha1BytesToString(_latestBadCert.cert.sha1);
|
|
|
|
|
|
|
|
/// Whitelist the last bad cert
|
2024-06-19 15:07:18 +02:00
|
|
|
Future<CertInfo> whitelistLastBadCert() async {
|
2021-05-09 12:31:27 +02:00
|
|
|
final info = await _writeCert(_latestBadCert.host, _latestBadCert.cert);
|
|
|
|
_whitelist.add(info);
|
2024-06-19 15:07:18 +02:00
|
|
|
unawaited(SelfSignedCert.reload());
|
|
|
|
return info;
|
2021-05-09 12:31:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Clear all whitelisted certs and they will no longer be allowed afterwards
|
|
|
|
Future<void> clearWhitelist() async {
|
|
|
|
final certDir = await _openCertsDir();
|
|
|
|
await certDir.delete(recursive: true);
|
|
|
|
_whitelist.clear();
|
|
|
|
return SelfSignedCert.reload();
|
|
|
|
}
|
|
|
|
|
2024-06-19 15:07:18 +02:00
|
|
|
Future<bool> removeFromWhitelist(CertInfo cert) async {
|
|
|
|
final certDir = await _openCertsDir();
|
|
|
|
final certFiles = (await certDir.list().toList()).whereType<File>();
|
|
|
|
for (final f in certFiles) {
|
|
|
|
if (!f.path.endsWith(".json")) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
final info = CertInfo.fromJson(jsonDecode(await f.readAsString()));
|
|
|
|
if (info == cert) {
|
|
|
|
final pemF = File(f.path.slice(0, -5));
|
|
|
|
await Future.wait([f.delete(), pemF.delete()]);
|
|
|
|
_log.info(
|
|
|
|
"[removeFromWhitelist] File removed: ${f.path}, ${pemF.path}");
|
|
|
|
unawaited(SelfSignedCert.reload());
|
|
|
|
_whitelist.remove(cert);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
} catch (e, stacktrace) {
|
|
|
|
_log.severe(
|
|
|
|
"[removeFromWhitelist] Failed to read certificate file: ${path_lib.basename(f.path)}",
|
|
|
|
e,
|
|
|
|
stacktrace);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
List<CertInfo> get whitelist => _whitelist.toList();
|
|
|
|
|
2021-05-09 12:31:27 +02:00
|
|
|
/// Read and return all persisted certificate infos
|
2024-06-19 15:07:18 +02:00
|
|
|
Future<List<CertInfo>> _readAllCerts() async {
|
|
|
|
final products = <CertInfo>[];
|
2021-05-09 12:31:27 +02:00
|
|
|
final certDir = await _openCertsDir();
|
|
|
|
final certFiles = (await certDir.list().toList()).whereType<File>();
|
|
|
|
for (final f in certFiles) {
|
|
|
|
if (!f.path.endsWith(".json")) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
try {
|
2024-06-19 15:07:18 +02:00
|
|
|
final info = CertInfo.fromJson(jsonDecode(await f.readAsString()));
|
2021-05-09 12:31:27 +02:00
|
|
|
_log.info(
|
2022-01-31 10:47:37 +01:00
|
|
|
"[_readAllCerts] Found certificate info: ${path_lib.basename(f.path)} for host: ${info.host}");
|
2021-05-09 12:31:27 +02:00
|
|
|
products.add(info);
|
|
|
|
} catch (e, stacktrace) {
|
|
|
|
_log.severe(
|
2022-01-31 10:47:37 +01:00
|
|
|
"[_readAllCerts] Failed to read certificate file: ${path_lib.basename(f.path)}",
|
2021-05-09 12:31:27 +02:00
|
|
|
e,
|
|
|
|
stacktrace);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return products;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Persist a new cert and return the info object
|
2024-06-19 15:07:18 +02:00
|
|
|
Future<CertInfo> _writeCert(String host, X509Certificate cert) async {
|
2021-05-09 12:31:27 +02:00
|
|
|
final certDir = await _openCertsDir();
|
|
|
|
while (true) {
|
2021-09-15 08:58:06 +02:00
|
|
|
final fileName = const Uuid().v4();
|
2021-05-09 12:31:27 +02:00
|
|
|
final certF = File("${certDir.path}/$fileName");
|
|
|
|
if (await certF.exists()) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
await certF.writeAsString(cert.pem, flush: true);
|
|
|
|
|
|
|
|
final siteF = File("${certDir.path}/$fileName.json");
|
2024-06-19 15:07:18 +02:00
|
|
|
final certInfo = CertInfo.fromX509Certificate(host, cert);
|
2021-05-09 12:31:27 +02:00
|
|
|
await siteF.writeAsString(jsonEncode(certInfo.toJson()), flush: true);
|
|
|
|
_log.info(
|
|
|
|
"[_persistBadCert] Persisted cert at '${certF.path}' for host '${_latestBadCert.host}'");
|
|
|
|
return certInfo;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<Directory> _openCertsDir() async {
|
|
|
|
final privateDir = await getApplicationSupportDirectory();
|
|
|
|
final certDir = Directory("${privateDir.path}/certs");
|
|
|
|
if (!await certDir.exists()) {
|
|
|
|
return certDir.create();
|
|
|
|
} else {
|
|
|
|
return certDir;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-23 22:05:57 +02:00
|
|
|
late _BadCertInfo _latestBadCert;
|
2024-06-19 15:07:18 +02:00
|
|
|
var _whitelist = <CertInfo>[];
|
2021-05-09 12:31:27 +02:00
|
|
|
|
2021-09-15 08:58:06 +02:00
|
|
|
static final _inst = SelfSignedCertManager._();
|
2021-05-09 12:31:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Modifications to this class must also reflect on Android side
|
2024-06-19 15:07:18 +02:00
|
|
|
final class CertInfo with EquatableMixin {
|
|
|
|
const CertInfo({
|
|
|
|
required this.host,
|
|
|
|
required this.sha1,
|
|
|
|
required this.subject,
|
|
|
|
required this.issuer,
|
|
|
|
required this.startValidity,
|
|
|
|
required this.endValidity,
|
|
|
|
});
|
|
|
|
|
|
|
|
factory CertInfo.fromX509Certificate(String host, X509Certificate cert) {
|
|
|
|
return CertInfo(
|
|
|
|
host: host,
|
|
|
|
sha1: _sha1BytesToString(cert.sha1),
|
|
|
|
subject: cert.subject,
|
|
|
|
issuer: cert.issuer,
|
|
|
|
startValidity: cert.startValidity,
|
|
|
|
endValidity: cert.endValidity,
|
2021-05-09 12:31:27 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-08-06 19:11:00 +02:00
|
|
|
JsonObj toJson() {
|
2021-05-09 12:31:27 +02:00
|
|
|
return {
|
|
|
|
"host": host,
|
|
|
|
"sha1": sha1,
|
|
|
|
"subject": subject,
|
|
|
|
"issuer": issuer,
|
2021-06-21 11:10:38 +02:00
|
|
|
"startValidity": startValidity.toUtc().toIso8601String(),
|
|
|
|
"endValidity": endValidity.toUtc().toIso8601String(),
|
2021-05-09 12:31:27 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-06-19 15:07:18 +02:00
|
|
|
factory CertInfo.fromJson(JsonObj json) {
|
|
|
|
return CertInfo(
|
|
|
|
host: json["host"],
|
|
|
|
sha1: json["sha1"],
|
|
|
|
subject: json["subject"],
|
|
|
|
issuer: json["issuer"],
|
|
|
|
startValidity: DateTime.parse(json["startValidity"]),
|
|
|
|
endValidity: DateTime.parse(json["endValidity"]),
|
2021-05-09 12:31:27 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-06-19 15:07:18 +02:00
|
|
|
@override
|
|
|
|
List<Object?> get props => [
|
|
|
|
host,
|
|
|
|
sha1,
|
|
|
|
subject,
|
|
|
|
issuer,
|
|
|
|
startValidity,
|
|
|
|
endValidity,
|
|
|
|
];
|
|
|
|
|
2021-05-09 12:31:27 +02:00
|
|
|
final String host;
|
|
|
|
final String sha1;
|
|
|
|
final String subject;
|
|
|
|
final String issuer;
|
|
|
|
final DateTime startValidity;
|
|
|
|
final DateTime endValidity;
|
|
|
|
}
|
|
|
|
|
|
|
|
class _BadCertInfo {
|
|
|
|
_BadCertInfo(this.cert, this.host, this.port);
|
|
|
|
|
|
|
|
final X509Certificate cert;
|
|
|
|
final String host;
|
|
|
|
final int port;
|
|
|
|
}
|
|
|
|
|
2022-12-16 16:01:04 +01:00
|
|
|
@npLog
|
2021-05-09 12:31:27 +02:00
|
|
|
class _CustomHttpOverrides extends HttpOverrides {
|
|
|
|
@override
|
2021-07-23 22:05:57 +02:00
|
|
|
HttpClient createHttpClient(SecurityContext? context) {
|
2021-05-09 12:31:27 +02:00
|
|
|
return super.createHttpClient(context)
|
|
|
|
..badCertificateCallback = (cert, host, port) {
|
|
|
|
try {
|
|
|
|
if (SelfSignedCertManager().verify(cert, host, port)) {
|
|
|
|
// _log.warning(
|
|
|
|
// "[badCertificateCallback] Allowing whitelisted self-signed cert");
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
} catch (e, stacktrace) {
|
|
|
|
_log.shout("[badCertificateCallback] Failed while verifying cert", e,
|
|
|
|
stacktrace);
|
|
|
|
}
|
|
|
|
SelfSignedCertManager()._latestBadCert = _BadCertInfo(cert, host, port);
|
|
|
|
return false;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
String _sha1BytesToString(Uint8List bytes) =>
|
|
|
|
bytes.map((e) => e.toRadixString(16).padLeft(2, "0")).join();
|