mirror of
https://github.com/localsend/localsend.git
synced 2026-08-07 07:14:52 +00:00
refactor: extract cert generation to core/
This commit is contained in:
@@ -166,7 +166,7 @@ class PersistenceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (prefs.getString(_securityContext) == null) {
|
if (prefs.getString(_securityContext) == null) {
|
||||||
await prefs.setString(_securityContext, jsonEncode(generateSecurityContext()));
|
await prefs.setString(_securityContext, jsonEncode(await generateSecurityContext()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFirstAppStart) {
|
if (isFirstAppStart) {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class SecurityService extends ReduxNotifier<StoredSecurityContext> {
|
|||||||
class ResetSecurityContextAction extends AsyncReduxAction<SecurityService, StoredSecurityContext> {
|
class ResetSecurityContextAction extends AsyncReduxAction<SecurityService, StoredSecurityContext> {
|
||||||
@override
|
@override
|
||||||
Future<StoredSecurityContext> reduce() async {
|
Future<StoredSecurityContext> reduce() async {
|
||||||
final securityContext = generateSecurityContext();
|
final securityContext = await generateSecurityContext();
|
||||||
await notifier._persistence.setSecurityContext(securityContext);
|
await notifier._persistence.setSecurityContext(securityContext);
|
||||||
return securityContext;
|
return securityContext;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +1,17 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:basic_utils/basic_utils.dart';
|
|
||||||
import 'package:convert/convert.dart';
|
|
||||||
import 'package:localsend_isolates/model/stored_security_context.dart';
|
import 'package:localsend_isolates/model/stored_security_context.dart';
|
||||||
import 'package:localsend_isolates/rust/api/crypto.dart' as rust;
|
import 'package:localsend_isolates/rust/api/crypto.dart' as rust;
|
||||||
|
|
||||||
/// Generates a random [SecurityContextResult].
|
/// Generates a random [StoredSecurityContext].
|
||||||
StoredSecurityContext generateSecurityContext([AsymmetricKeyPair? keyPair]) {
|
Future<StoredSecurityContext> generateSecurityContext() async {
|
||||||
keyPair ??= CryptoUtils.generateRSAKeyPair();
|
final result = await rust.generateSecurityContext();
|
||||||
final privateKey = keyPair.privateKey as RSAPrivateKey;
|
|
||||||
final publicKey = keyPair.publicKey as RSAPublicKey;
|
|
||||||
final dn = {
|
|
||||||
'CN': 'LocalSend User',
|
|
||||||
'O': '',
|
|
||||||
'OU': '',
|
|
||||||
'L': '',
|
|
||||||
'S': '',
|
|
||||||
'C': '',
|
|
||||||
};
|
|
||||||
final csr = X509Utils.generateRsaCsrPem(dn, privateKey, publicKey);
|
|
||||||
final certificate = X509Utils.generateSelfSignedCertificate(keyPair.privateKey, csr, 365 * 10);
|
|
||||||
|
|
||||||
final hash = calculateHashOfCertificate(certificate);
|
|
||||||
final spki = extractPublicKeyFromCertificate(certificate);
|
|
||||||
|
|
||||||
return StoredSecurityContext(
|
return StoredSecurityContext(
|
||||||
privateKey: CryptoUtils.encodeRSAPrivateKeyToPemPkcs1(privateKey),
|
privateKey: result.privateKey,
|
||||||
publicKey: spki,
|
publicKey: result.publicKey,
|
||||||
certificate: certificate,
|
certificate: result.certificate,
|
||||||
certificateHash: hash,
|
certificateHash: result.certificateHash,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculates the hash of a certificate.
|
|
||||||
String calculateHashOfCertificate(String certificate) {
|
|
||||||
// Convert PEM to DER
|
|
||||||
final pemContent = certificate.replaceAll('\r\n', '\n').split('\n').where((line) => line.isNotEmpty && !line.startsWith('---')).join();
|
|
||||||
final der = base64Decode(pemContent);
|
|
||||||
|
|
||||||
// Calculate hash
|
|
||||||
return CryptoUtils.getHash(
|
|
||||||
Uint8List.fromList(der),
|
|
||||||
algorithmName: 'SHA-256',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String extractPublicKeyFromCertificate(String certificate) {
|
|
||||||
final cert = X509Utils.x509CertificateFromPem(certificate);
|
|
||||||
final publicHex = cert.tbsCertificate!.subjectPublicKeyInfo.bytes!;
|
|
||||||
return _hexToSpkiPem(publicHex);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _hexToSpkiPem(String hexBytes) {
|
|
||||||
final publicBytes = hex.decode(hexBytes);
|
|
||||||
final publicBase64 = base64Encode(publicBytes);
|
|
||||||
final temp =
|
|
||||||
'''-----BEGIN PUBLIC KEY-----
|
|
||||||
$publicBase64
|
|
||||||
-----END PUBLIC KEY-----''';
|
|
||||||
return X509Utils.fixPem(temp);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verifies a certificate with a public key.
|
/// Verifies a certificate with a public key.
|
||||||
/// Throws an exception if the certificate is invalid.
|
/// Throws an exception if the certificate is invalid.
|
||||||
Future<void> verifyCertificate({
|
Future<void> verifyCertificate({
|
||||||
|
|||||||
+1
-17
@@ -73,14 +73,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.13.1"
|
version: "2.13.1"
|
||||||
basic_utils:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: basic_utils
|
|
||||||
sha256: "548047bef0b3b697be19fa62f46de54d99c9019a69fb7db92c69e19d87f633c7"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "5.8.2"
|
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -226,7 +218,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.0"
|
||||||
convert:
|
convert:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: convert
|
name: convert
|
||||||
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
|
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
|
||||||
@@ -1263,14 +1255,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.8"
|
version: "2.1.8"
|
||||||
pointycastle:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: pointycastle
|
|
||||||
sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "4.0.0"
|
|
||||||
pool:
|
pool:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -10,10 +10,8 @@ environment:
|
|||||||
sdk: ^3.11.0
|
sdk: ^3.11.0
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
basic_utils: 5.8.2
|
|
||||||
collection: ^1.17.2 # allow newer versions, so it can compile with newer Flutter versions
|
collection: ^1.17.2 # allow newer versions, so it can compile with newer Flutter versions
|
||||||
connectivity_plus: 7.2.0
|
connectivity_plus: 7.2.0
|
||||||
convert: 3.1.2
|
|
||||||
dart_mappable: 4.8.0
|
dart_mappable: 4.8.0
|
||||||
desktop_drop: 0.7.1
|
desktop_drop: 0.7.1
|
||||||
device_apps: 2.2.0
|
device_apps: 2.2.0
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
import 'package:basic_utils/basic_utils.dart';
|
|
||||||
import 'package:localsend_app/util/security_helper.dart';
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
group('generateSecurityContext', () {
|
|
||||||
// certificate is dependent on time, so we skip this test
|
|
||||||
test('should generate a security context', () {
|
|
||||||
const modulus =
|
|
||||||
'17655370025740741038113156454854199388193503449104440877037318492305460691763108986359629213738068251658702764711191809795804339624629386590749322066892437190139062703234048708020881575102122778994526543290880868262952627726488977960776519938694907994518427540419624537894956809241753063372413058294447378662255052300193831939738828105579615087656442141880311261067266950751778487270744852541404898857201772242498437323791185553734672978477936608290952211534467577849886694581791190994405933665028620559373744808123499229584489594678456882464781972729864955090923226165749030219906764402228914184366893373631422503021';
|
|
||||||
const privateExponent =
|
|
||||||
'14421008176265737041842552728760854897987189421761902811979258986703749275840685218255600827633436556136869519473805770330945910011841184271318521744401163544644005150782334580299156378790480712915696482733480991790654395453357362699213084041660086971551428119477591606848878236042545176547425781216107887562053872086603693550866169434312772263069345523523039280590638079481520003143359443743141464111595442124530326974852967024986370324441963907065113439224776888067662971143404975436160178871895218249902778285379960904695389191962540588337134680308483675532942364276364403564632886857709821444086544738315317174145';
|
|
||||||
const privateP =
|
|
||||||
'178524065934350447993678840182944627942309006340524319909851831752106332954026000610471033106817833873729348468467401050633468292518369706764968836071362023882930741070266054692417392199462715540565991951570137107225695543150432250384310373010291328019775467135529294964213555809388513634733256540639643918349';
|
|
||||||
const privateQ =
|
|
||||||
'98896302486373120089171491460892017227056533661820429564718404666288200233919205658787592871460875274200632466484814562010402123837563042503616158962868253836369412258319855713924996179145010239261725685588005640861685432715579085801665010203646507394652948031357507454692937609788924433344012456182622364129';
|
|
||||||
const publicExponent = '65537';
|
|
||||||
|
|
||||||
final keyPair = AsymmetricKeyPair(
|
|
||||||
RSAPublicKey(
|
|
||||||
BigInt.parse(modulus),
|
|
||||||
BigInt.parse(publicExponent),
|
|
||||||
),
|
|
||||||
RSAPrivateKey(
|
|
||||||
BigInt.parse(modulus),
|
|
||||||
BigInt.parse(privateExponent),
|
|
||||||
BigInt.parse(privateP),
|
|
||||||
BigInt.parse(privateQ),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final context = generateSecurityContext(keyPair);
|
|
||||||
expect(
|
|
||||||
context.privateKey,
|
|
||||||
'''-----BEGIN RSA PRIVATE KEY-----
|
|
||||||
MIIEpAIBAAKCAQEAi9uDMYRn63SZtEPGRogZGdu5XXBAoQeMO60mycoinqLKDWyZ
|
|
||||||
dpMo+XWY3wYVhoAyxgzDOcPjIf+Uq1oEy/0K4WwfpbK8SCy851qgYkfMCT9D9mFv
|
|
||||||
XwWoULJCUHFF7f947ArDE1nmuK1nNx2RodN2wJCXyzPjw0jn06bwGeg0EqfUC8wv
|
|
||||||
W4FTZ6t1tErzmRqRdMUWuCJwsk1IMbDbFePhiK5jecOBG0RVVWLuw+TkuX8TUgrp
|
|
||||||
IktH2+qEM1KdLyAMnL71hx2wMvE+lDKFKK9p37zXK8omjl+VgTC8ocjGeYDDsl43
|
|
||||||
ZtW09V0pb7Vz2FM8b7BgM06kvJl48PIe5puYbQIDAQABAoIBAHI8hAW/TA7j9+Rp
|
|
||||||
c5wK8M3RTrCGzxN5Ec9w2Hz80ZhYCcg7S4KyI0bYCl0pIA3zywVASXp2iaEsnSf1
|
|
||||||
aHOipe+JHLSAsRAXEtm6icSdNojbF005OvoXqer4H/iK/X6wLPpItByrUyzH3sYD
|
|
||||||
LgBLtPUHZiWBpenONCYKdpYCt/3/uyAeC4qj+sBlfN5Fio2A86qIgntg3n6he8Cd
|
|
||||||
Yq0cjyEGzRnIa9EUDHoHUU30oPPj16LkK0uZxR7H+UKZkYNXQUGtPt0cnAvgS/2f
|
|
||||||
UnOAIZop1r9NkS7sntg4l9iuDqI4xYMg9wtBStrgLx65nV1l4HDNZGq/LDo3msam
|
|
||||||
NQoeX4ECgYEA/joJk42iTF3D2IJjFzMf539V9D/1OaXcxmiLMODsIW8ZcyKRRmzD
|
|
||||||
NIBfMxX/0+X8IgkMwkcnZWOFMWw+2FDgfz2l+PMACJvGEGokxFDiJe68W4KAy8dr
|
|
||||||
uB0M0ydhRWLG4ZD6U+gvwyqqlMNpEosCH0fJilag+tW/fx6ti2MBYA0CgYEAjNVA
|
|
||||||
HItnlpnOmzOuyQob9kqE/8eDTSeX5ZFD/OZOypvLPukdg6qWnzaBzUgSJg/Tl9RD
|
|
||||||
XWE9WgKc2Gpki7/EYvtRW4YwBcoZKBTkAio4t8WBRmLT6L3PUKKdJHOtFbmwlUPw
|
|
||||||
mfw9HdJG/TgDiTlyAdTbuGhGY/unuUdPMvt2oeECgYEApthqHoeOo3XKKZbw93Hb
|
|
||||||
F3AvdhxfkVT0ftZvu0VyU0L5veFK3KBWwGcbk4h1nJjMj33G/N370gOtj1EOMaNq
|
|
||||||
ordP7QF13TB2naE7vgejU+fJgHk2lAauAGg4WX/3y7TW94TRdS3l4r1mtDlHBR9r
|
|
||||||
5iGT+JGAFv8fLYtxtA/nACUCgYAb6Gpi/bESY/pQQSaiyjEOVmgSs7uuP2lXYbkC
|
|
||||||
VbVJayQUnGdv3w8oD8obHuwRxNMeZD7RM2LQAnKIZFT2aJMHNlxB8c50Zz8i9TjV
|
|
||||||
wP4qVKYwh4cMuQhrJz5SqeWjx39ZpPP538VQsonExiPVPp/8Au1jlq5UQ9tR2PK1
|
|
||||||
3KT+oQKBgQCkmNd8ZHKBoIbG3oV0T4f4IFk8424QD5hQX7Zmb7gxwnCgQScOPBM/
|
|
||||||
tk4v/rwNoiO9EUW4w4zZZIlvcFJu38+9pPX+rTFxGh6TZ6aRvw7962m2RBmqgYcq
|
|
||||||
IWwwBbDLI6KuU/iqqvk/1syLDHqeaCdDqTmmyoaKKa7kUhkZkhIlLw==
|
|
||||||
-----END RSA PRIVATE KEY-----''',
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
context.publicKey,
|
|
||||||
'''-----BEGIN PUBLIC KEY-----
|
|
||||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAi9uDMYRn63SZtEPGRogZ
|
|
||||||
Gdu5XXBAoQeMO60mycoinqLKDWyZdpMo+XWY3wYVhoAyxgzDOcPjIf+Uq1oEy/0K
|
|
||||||
4WwfpbK8SCy851qgYkfMCT9D9mFvXwWoULJCUHFF7f947ArDE1nmuK1nNx2RodN2
|
|
||||||
wJCXyzPjw0jn06bwGeg0EqfUC8wvW4FTZ6t1tErzmRqRdMUWuCJwsk1IMbDbFePh
|
|
||||||
iK5jecOBG0RVVWLuw+TkuX8TUgrpIktH2+qEM1KdLyAMnL71hx2wMvE+lDKFKK9p
|
|
||||||
37zXK8omjl+VgTC8ocjGeYDDsl43ZtW09V0pb7Vz2FM8b7BgM06kvJl48PIe5puY
|
|
||||||
bQIDAQAB
|
|
||||||
-----END PUBLIC KEY-----''',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('calculateHashOfCertificate', () {
|
|
||||||
test('should calculate hash of certificate', () {
|
|
||||||
const certificate = '''-----BEGIN CERTIFICATE-----
|
|
||||||
MIIDGTCCAgGgAwIBAgIBATANBgkqhkiG9w0BAQsFADBQMRcwFQYDVQQDEw5Mb2Nh
|
|
||||||
bFNlbmQgVXNlcjEJMAcGA1UEChMAMQkwBwYDVQQLEwAxCTAHBgNVBAcTADEJMAcG
|
|
||||||
A1UECBMAMQkwBwYDVQQGEwAwHhcNMjMwNDIxMjM0NTM3WhcNMzMwNDE4MjM0NTM3
|
|
||||||
WjBQMRcwFQYDVQQDEw5Mb2NhbFNlbmQgVXNlcjEJMAcGA1UEChMAMQkwBwYDVQQL
|
|
||||||
EwAxCTAHBgNVBAcTADEJMAcGA1UECBMAMQkwBwYDVQQGEwAwggEiMA0GCSqGSIb3
|
|
||||||
DQEBAQUAA4IBDwAwggEKAoIBAQCL24MxhGfrdJm0Q8ZGiBkZ27ldcEChB4w7rSbJ
|
|
||||||
yiKeosoNbJl2kyj5dZjfBhWGgDLGDMM5w+Mh/5SrWgTL/QrhbB+lsrxILLznWqBi
|
|
||||||
R8wJP0P2YW9fBahQskJQcUXt/3jsCsMTWea4rWc3HZGh03bAkJfLM+PDSOfTpvAZ
|
|
||||||
6DQSp9QLzC9bgVNnq3W0SvOZGpF0xRa4InCyTUgxsNsV4+GIrmN5w4EbRFVVYu7D
|
|
||||||
5OS5fxNSCukiS0fb6oQzUp0vIAycvvWHHbAy8T6UMoUor2nfvNcryiaOX5WBMLyh
|
|
||||||
yMZ5gMOyXjdm1bT1XSlvtXPYUzxvsGAzTqS8mXjw8h7mm5htAgMBAAEwDQYJKoZI
|
|
||||||
hvcNAQELBQADggEBAIs+T8Nkbl0gecT22CKW9/jMvUS1PGAyMqlwP8fNTsyv2xE9
|
|
||||||
hLsyUrxsscuv+HGJu6Cz1R3hLI8YY5jEShmaelI0stlLahH9Fbm43EZuadGXOVKZ
|
|
||||||
gMrNzQqLY5lec55rmS17GJlkm5opidkq4OlsCHfrBJitX6071atb0B1cdAjysWwV
|
|
||||||
x40mnwq0TmYgBLDhWaM4/ZfZQRJQpPCtBJO06Nk7gTPiqJGJU5iEaz1PLvARq69o
|
|
||||||
bJobSekf9tx3uwOIfioaoQvX0khkZ3ljFuNUpW3IE87OfPnYJQhu5xsTx00wi+Ce
|
|
||||||
x64ghD4CzRa7wYsOjeb8cUUDMSj030NO9fBGVtA=
|
|
||||||
-----END CERTIFICATE-----''';
|
|
||||||
|
|
||||||
final hash = calculateHashOfCertificate(certificate);
|
|
||||||
expect(hash, '247E5F7CF21DE14438EAE733E07AC5440593D0612570C7413674130608DF69A9');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Generated
+1
@@ -1812,6 +1812,7 @@ dependencies = [
|
|||||||
"pem 3.0.6",
|
"pem 3.0.6",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"rand 0.9.5",
|
"rand 0.9.5",
|
||||||
|
"rcgen 0.14.8",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rsa",
|
"rsa",
|
||||||
"rustls",
|
"rustls",
|
||||||
|
|||||||
@@ -86,20 +86,14 @@ impl Identity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn generate(alias: String, port: u16) -> anyhow::Result<Self> {
|
fn generate(alias: String, port: u16) -> anyhow::Result<Self> {
|
||||||
let key_pair = rcgen::KeyPair::generate()?;
|
let cert = localsend::crypto::cert::generate_self_signed()?;
|
||||||
let mut params = rcgen::CertificateParams::default();
|
|
||||||
params.distinguished_name = rcgen::DistinguishedName::new();
|
|
||||||
params
|
|
||||||
.distinguished_name
|
|
||||||
.push(rcgen::DnType::CommonName, "LocalSend User");
|
|
||||||
let cert = params.self_signed(&key_pair)?;
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
alias,
|
alias,
|
||||||
port,
|
port,
|
||||||
fingerprint: fingerprint_from_cert_der(cert.der()),
|
fingerprint: cert.fingerprint,
|
||||||
cert_pem: cert.pem(),
|
cert_pem: cert.certificate_pem,
|
||||||
key_pem: key_pair.serialize_pem(),
|
key_pem: cert.private_key_pem,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Generated
+37
-4
@@ -190,6 +190,15 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bit-vec"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.3.2"
|
version = "1.3.2"
|
||||||
@@ -549,7 +558,7 @@ dependencies = [
|
|||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
"rand 0.9.2",
|
"rand 0.9.2",
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
"rcgen",
|
"rcgen 0.13.2",
|
||||||
"ring",
|
"ring",
|
||||||
"rustls",
|
"rustls",
|
||||||
"sec1",
|
"sec1",
|
||||||
@@ -1262,7 +1271,7 @@ dependencies = [
|
|||||||
"pem",
|
"pem",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"rand 0.9.2",
|
"rand 0.9.2",
|
||||||
"rcgen",
|
"rcgen 0.14.8",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rsa",
|
"rsa",
|
||||||
"rustls",
|
"rustls",
|
||||||
@@ -1756,7 +1765,21 @@ dependencies = [
|
|||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"time",
|
"time",
|
||||||
"x509-parser 0.16.0",
|
"x509-parser 0.16.0",
|
||||||
"yasna",
|
"yasna 0.5.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rcgen"
|
||||||
|
version = "0.14.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "57f6d249aad744e274e682777a50283a225a32705394ee6d5fcc01efa25e4055"
|
||||||
|
dependencies = [
|
||||||
|
"pem",
|
||||||
|
"ring",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"time",
|
||||||
|
"x509-parser 0.18.0",
|
||||||
|
"yasna 0.6.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2908,7 +2931,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
"rand 0.9.2",
|
"rand 0.9.2",
|
||||||
"rcgen",
|
"rcgen 0.13.2",
|
||||||
"regex",
|
"regex",
|
||||||
"ring",
|
"ring",
|
||||||
"rtcp",
|
"rtcp",
|
||||||
@@ -3418,6 +3441,16 @@ dependencies = [
|
|||||||
"time",
|
"time",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yasna"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282"
|
||||||
|
dependencies = [
|
||||||
|
"bit-vec",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yoke"
|
name = "yoke"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ hyper-util = { version = "0.1.19", features = ["server"], optional = true }
|
|||||||
lru = "0.16.3"
|
lru = "0.16.3"
|
||||||
pem = { version = "3.0.6", optional = true }
|
pem = { version = "3.0.6", optional = true }
|
||||||
percent-encoding = { version = "2.3", optional = true }
|
percent-encoding = { version = "2.3", optional = true }
|
||||||
|
rcgen = { version = "0.14", default-features = false, features = ["crypto", "pem", "ring"], optional = true }
|
||||||
reqwest = { version = "0.13.1", features = ["charset", "http2", "system-proxy", "json", "rustls-no-provider", "stream", "webpki-roots"], default-features = false, optional = true }
|
reqwest = { version = "0.13.1", features = ["charset", "http2", "system-proxy", "json", "rustls-no-provider", "stream", "webpki-roots"], default-features = false, optional = true }
|
||||||
rand = "0.9.1"
|
rand = "0.9.1"
|
||||||
rsa = { version = "0.9.8", optional = true }
|
rsa = { version = "0.9.8", optional = true }
|
||||||
@@ -39,14 +40,19 @@ uuid = { version = "1.20.0", features = ["serde", "v4"] }
|
|||||||
webrtc = { version = "0.14.0", optional = true }
|
webrtc = { version = "0.14.0", optional = true }
|
||||||
x509-parser = { version = "0.18.0", features = ["verify"], optional = true }
|
x509-parser = { version = "0.18.0", features = ["verify"], optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
rcgen = "0.13.2"
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
crypto = ["ed25519-dalek", "rsa", "sha2", "tokio-util"]
|
crypto = ["ed25519-dalek", "rcgen", "rsa", "sha2", "tokio-util"]
|
||||||
http = ["crypto", "form_urlencoded", "http-body-util", "hyper", "hyper-util", "pem", "percent-encoding", "reqwest", "rustls", "socket2", "tokio-rustls", "tokio-util", "x509-parser"]
|
http = ["crypto", "form_urlencoded", "http-body-util", "hyper", "hyper-util", "pem", "percent-encoding", "reqwest", "rustls", "socket2", "tokio-rustls", "tokio-util", "x509-parser"]
|
||||||
multicast = ["if-addrs", "socket2", "tokio-util"]
|
multicast = ["if-addrs", "socket2", "tokio-util"]
|
||||||
webrtc-signaling = ["tokio-tungstenite"]
|
webrtc-signaling = ["tokio-tungstenite"]
|
||||||
webrtc = ["crypto", "flate2", "dep:webrtc", "webrtc-signaling", "x509-parser"]
|
webrtc = ["crypto", "flate2", "dep:webrtc", "webrtc-signaling", "x509-parser"]
|
||||||
full = ["crypto", "http", "multicast", "webrtc"]
|
full = ["crypto", "http", "multicast", "webrtc"]
|
||||||
|
|
||||||
|
# RSA key generation is bignum-heavy and takes ~10x longer unoptimized;
|
||||||
|
# keep the crypto crates optimized in dev so tests stay fast.
|
||||||
|
[profile.dev.package.rsa]
|
||||||
|
opt-level = 2
|
||||||
|
|
||||||
|
[profile.dev.package.num-bigint-dig]
|
||||||
|
opt-level = 2
|
||||||
|
|||||||
@@ -4,6 +4,57 @@ use x509_parser::certificate::X509Certificate;
|
|||||||
use x509_parser::pem::Pem;
|
use x509_parser::pem::Pem;
|
||||||
use x509_parser::x509::SubjectPublicKeyInfo;
|
use x509_parser::x509::SubjectPublicKeyInfo;
|
||||||
|
|
||||||
|
/// A freshly generated device identity: an RSA-2048 key pair and a
|
||||||
|
/// self-signed certificate whose SHA-256 fingerprint identifies the device.
|
||||||
|
pub struct SelfSignedCert {
|
||||||
|
/// The private key, PEM-encoded (PKCS#8).
|
||||||
|
pub private_key_pem: String,
|
||||||
|
/// The public key, PEM-encoded (SPKI).
|
||||||
|
pub public_key_pem: String,
|
||||||
|
/// The self-signed certificate, PEM-encoded.
|
||||||
|
pub certificate_pem: String,
|
||||||
|
/// The SHA-256 fingerprint of the certificate in DER format,
|
||||||
|
/// encoded as uppercase hex (see [fingerprint_from_cert_der]).
|
||||||
|
pub fingerprint: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates a new device identity, used for both the HTTP server and client
|
||||||
|
/// certificates.
|
||||||
|
///
|
||||||
|
/// - RSA-2048, matching the certificates the Flutter app has historically
|
||||||
|
/// generated in Dart.
|
||||||
|
/// - `CN=LocalSend User` and no SANs: peers identify each other purely by the
|
||||||
|
/// certificate fingerprint, so the name carries no information.
|
||||||
|
/// - The serial number is derived from the hash of the public key
|
||||||
|
/// (rcgen's default when no serial number is set).
|
||||||
|
/// - Validity is rcgen's default (1975 to 4096), so certificates do not expire
|
||||||
|
/// in practice and never need to be rotated for time reasons.
|
||||||
|
pub fn generate_self_signed() -> anyhow::Result<SelfSignedCert> {
|
||||||
|
use rsa::pkcs8::{EncodePrivateKey, EncodePublicKey, LineEnding};
|
||||||
|
|
||||||
|
let mut rng = rsa::rand_core::OsRng;
|
||||||
|
let private_key = rsa::RsaPrivateKey::new(&mut rng, 2048)?;
|
||||||
|
let private_key_pem = private_key.to_pkcs8_pem(LineEnding::LF)?;
|
||||||
|
let public_key_pem = private_key
|
||||||
|
.to_public_key()
|
||||||
|
.to_public_key_pem(LineEnding::LF)?;
|
||||||
|
|
||||||
|
let key_pair = rcgen::KeyPair::try_from(private_key.to_pkcs8_der()?.as_bytes())?;
|
||||||
|
let mut params = rcgen::CertificateParams::default();
|
||||||
|
params.distinguished_name = rcgen::DistinguishedName::new();
|
||||||
|
params
|
||||||
|
.distinguished_name
|
||||||
|
.push(rcgen::DnType::CommonName, "LocalSend User");
|
||||||
|
let certificate = params.self_signed(&key_pair)?;
|
||||||
|
|
||||||
|
Ok(SelfSignedCert {
|
||||||
|
private_key_pem: private_key_pem.to_string(),
|
||||||
|
public_key_pem,
|
||||||
|
certificate_pem: certificate.pem(),
|
||||||
|
fingerprint: fingerprint_from_cert_der(certificate.der()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn verify_cert_from_pem(cert: String, public_key: Option<&str>) -> anyhow::Result<()> {
|
pub fn verify_cert_from_pem(cert: String, public_key: Option<&str>) -> anyhow::Result<()> {
|
||||||
let (cert_pem, _) = Pem::read(Cursor::new(cert.into_bytes()))?;
|
let (cert_pem, _) = Pem::read(Cursor::new(cert.into_bytes()))?;
|
||||||
let parsed_cert: X509Certificate = cert_pem.parse_x509()?;
|
let parsed_cert: X509Certificate = cert_pem.parse_x509()?;
|
||||||
@@ -188,6 +239,40 @@ nidU/qXQvBJ7NPUkXXgbcgqxK735iijOqQHmKts=
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_generate_self_signed() {
|
||||||
|
let generated = generate_self_signed().unwrap();
|
||||||
|
|
||||||
|
assert!(generated
|
||||||
|
.private_key_pem
|
||||||
|
.starts_with("-----BEGIN PRIVATE KEY-----"));
|
||||||
|
assert!(generated
|
||||||
|
.public_key_pem
|
||||||
|
.starts_with("-----BEGIN PUBLIC KEY-----"));
|
||||||
|
|
||||||
|
// The certificate is self-consistent and carries the generated public key.
|
||||||
|
verify_cert_from_pem(
|
||||||
|
generated.certificate_pem.clone(),
|
||||||
|
Some(&generated.public_key_pem),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// The fingerprint matches the DER bytes of the certificate.
|
||||||
|
let (cert_pem, _) =
|
||||||
|
Pem::read(Cursor::new(generated.certificate_pem.as_bytes().to_vec())).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
generated.fingerprint,
|
||||||
|
fingerprint_from_cert_der(&cert_pem.contents)
|
||||||
|
);
|
||||||
|
|
||||||
|
// The public key extracted from the certificate is the generated one.
|
||||||
|
let extracted = public_key_from_cert_der(&cert_pem.contents).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
extracted.replace("\r\n", "\n").trim(),
|
||||||
|
generated.public_key_pem.trim()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_fingerprint_from_cert_der() {
|
fn test_fingerprint_from_cert_der() {
|
||||||
let (cert_pem, _) = Pem::read(Cursor::new(CERT.as_bytes().to_vec())).unwrap();
|
let (cert_pem, _) = Pem::read(Cursor::new(CERT.as_bytes().to_vec())).unwrap();
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
use localsend::crypto::cert::fingerprint_from_cert_der;
|
|
||||||
use localsend::http::client::{ClientError, LsHttpClientV2};
|
use localsend::http::client::{ClientError, LsHttpClientV2};
|
||||||
use localsend::http::dto::ProtocolType;
|
use localsend::http::dto::ProtocolType;
|
||||||
use localsend::http::dto_v2::{PrepareUploadRequestDtoV2, ProtocolTypeV2, RegisterDtoV2};
|
use localsend::http::dto_v2::{PrepareUploadRequestDtoV2, ProtocolTypeV2, RegisterDtoV2};
|
||||||
@@ -30,16 +29,12 @@ struct Identity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn generate_identity() -> Identity {
|
fn generate_identity() -> Identity {
|
||||||
let key_pair = rcgen::KeyPair::generate().unwrap();
|
let cert = localsend::crypto::cert::generate_self_signed().unwrap();
|
||||||
let cert = rcgen::CertificateParams::new(vec!["LocalSend User".to_string()])
|
|
||||||
.unwrap()
|
|
||||||
.self_signed(&key_pair)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
Identity {
|
Identity {
|
||||||
cert: cert.pem(),
|
cert: cert.certificate_pem,
|
||||||
private_key: key_pair.serialize_pem(),
|
private_key: cert.private_key_pem,
|
||||||
fingerprint: fingerprint_from_cert_der(cert.der()),
|
fingerprint: cert.fingerprint,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ Future<void> verifyCert({required String cert, required String publicKey}) =>
|
|||||||
|
|
||||||
Future<KeyPair> generateKeyPair() => RustLib.instance.api.crateApiCryptoGenerateKeyPair();
|
Future<KeyPair> generateKeyPair() => RustLib.instance.api.crateApiCryptoGenerateKeyPair();
|
||||||
|
|
||||||
|
/// Generates a new device identity: an RSA-2048 key pair and a self-signed
|
||||||
|
/// certificate whose SHA-256 fingerprint identifies the device.
|
||||||
|
Future<SecurityContext> generateSecurityContext() => RustLib.instance.api.crateApiCryptoGenerateSecurityContext();
|
||||||
|
|
||||||
/// Computes the SHA-256 checksum of a file, encoded as lowercase hex.
|
/// Computes the SHA-256 checksum of a file, encoded as lowercase hex.
|
||||||
///
|
///
|
||||||
/// The file is read chunk by chunk, so it is never fully loaded into memory.
|
/// The file is read chunk by chunk, so it is never fully loaded into memory.
|
||||||
@@ -41,3 +45,30 @@ class KeyPair {
|
|||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
other is KeyPair && runtimeType == other.runtimeType && privateKey == other.privateKey && publicKey == other.publicKey;
|
other is KeyPair && runtimeType == other.runtimeType && privateKey == other.privateKey && publicKey == other.publicKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SecurityContext {
|
||||||
|
final String privateKey;
|
||||||
|
final String publicKey;
|
||||||
|
final String certificate;
|
||||||
|
final String certificateHash;
|
||||||
|
|
||||||
|
const SecurityContext({
|
||||||
|
required this.privateKey,
|
||||||
|
required this.publicKey,
|
||||||
|
required this.certificate,
|
||||||
|
required this.certificateHash,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => privateKey.hashCode ^ publicKey.hashCode ^ certificate.hashCode ^ certificateHash.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is SecurityContext &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
privateKey == other.privateKey &&
|
||||||
|
publicKey == other.publicKey &&
|
||||||
|
certificate == other.certificate &&
|
||||||
|
certificateHash == other.certificateHash;
|
||||||
|
}
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
|
|||||||
String get codegenVersion => '2.12.0';
|
String get codegenVersion => '2.12.0';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get rustContentHash => 1795427439;
|
int get rustContentHash => -286605518;
|
||||||
|
|
||||||
static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig(
|
static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig(
|
||||||
stem: 'rust_lib_localsend_app',
|
stem: 'rust_lib_localsend_app',
|
||||||
@@ -247,6 +247,8 @@ abstract class RustLibApi extends BaseApi {
|
|||||||
|
|
||||||
Future<KeyPair> crateApiCryptoGenerateKeyPair();
|
Future<KeyPair> crateApiCryptoGenerateKeyPair();
|
||||||
|
|
||||||
|
Future<SecurityContext> crateApiCryptoGenerateSecurityContext();
|
||||||
|
|
||||||
Future<String> crateApiCryptoHashFile({String? path, int? fileDescriptor, Uint8List? bytes, required RsCancellationToken cancelToken});
|
Future<String> crateApiCryptoHashFile({String? path, int? fileDescriptor, Uint8List? bytes, required RsCancellationToken cancelToken});
|
||||||
|
|
||||||
Future<RsMulticast> crateApiMulticastStartMulticast({
|
Future<RsMulticast> crateApiMulticastStartMulticast({
|
||||||
@@ -1670,6 +1672,30 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
argNames: [],
|
argNames: [],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<SecurityContext> crateApiCryptoGenerateSecurityContext() {
|
||||||
|
return handler.executeNormal(
|
||||||
|
NormalTask(
|
||||||
|
callFfi: (port_) {
|
||||||
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
|
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 45, port: port_);
|
||||||
|
},
|
||||||
|
codec: SseCodec(
|
||||||
|
decodeSuccessData: sse_decode_security_context,
|
||||||
|
decodeErrorData: sse_decode_AnyhowException,
|
||||||
|
),
|
||||||
|
constMeta: kCrateApiCryptoGenerateSecurityContextConstMeta,
|
||||||
|
argValues: [],
|
||||||
|
apiImpl: this,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskConstMeta get kCrateApiCryptoGenerateSecurityContextConstMeta => const TaskConstMeta(
|
||||||
|
debugName: 'generate_security_context',
|
||||||
|
argNames: [],
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String> crateApiCryptoHashFile({String? path, int? fileDescriptor, Uint8List? bytes, required RsCancellationToken cancelToken}) {
|
Future<String> crateApiCryptoHashFile({String? path, int? fileDescriptor, Uint8List? bytes, required RsCancellationToken cancelToken}) {
|
||||||
return handler.executeNormal(
|
return handler.executeNormal(
|
||||||
@@ -1680,7 +1706,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
sse_encode_opt_box_autoadd_i_32(fileDescriptor, serializer);
|
sse_encode_opt_box_autoadd_i_32(fileDescriptor, serializer);
|
||||||
sse_encode_opt_list_prim_u_8_strict(bytes, serializer);
|
sse_encode_opt_list_prim_u_8_strict(bytes, serializer);
|
||||||
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsCancellationToken(cancelToken, serializer);
|
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsCancellationToken(cancelToken, serializer);
|
||||||
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 45, port: port_);
|
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 46, port: port_);
|
||||||
},
|
},
|
||||||
codec: SseCodec(
|
codec: SseCodec(
|
||||||
decodeSuccessData: sse_decode_String,
|
decodeSuccessData: sse_decode_String,
|
||||||
@@ -1727,7 +1753,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
sse_encode_String(fingerprint, serializer);
|
sse_encode_String(fingerprint, serializer);
|
||||||
sse_encode_protocol_type_v_2(protocol, serializer);
|
sse_encode_protocol_type_v_2(protocol, serializer);
|
||||||
sse_encode_bool(download, serializer);
|
sse_encode_bool(download, serializer);
|
||||||
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 46, port: port_);
|
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 47, port: port_);
|
||||||
},
|
},
|
||||||
codec: SseCodec(
|
codec: SseCodec(
|
||||||
decodeSuccessData: sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast,
|
decodeSuccessData: sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast,
|
||||||
@@ -1784,7 +1810,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
sse_encode_opt_String(pin, serializer);
|
sse_encode_opt_String(pin, serializer);
|
||||||
sse_encode_opt_box_autoadd_web_send_params(webSend, serializer);
|
sse_encode_opt_box_autoadd_web_send_params(webSend, serializer);
|
||||||
sse_encode_opt_String(showToken, serializer);
|
sse_encode_opt_String(showToken, serializer);
|
||||||
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 47, port: port_);
|
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 48, port: port_);
|
||||||
},
|
},
|
||||||
codec: SseCodec(
|
codec: SseCodec(
|
||||||
decodeSuccessData: sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer,
|
decodeSuccessData: sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer,
|
||||||
@@ -1810,7 +1836,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
sse_encode_String(cert, serializer);
|
sse_encode_String(cert, serializer);
|
||||||
sse_encode_String(publicKey, serializer);
|
sse_encode_String(publicKey, serializer);
|
||||||
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 48, port: port_);
|
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 49, port: port_);
|
||||||
},
|
},
|
||||||
codec: SseCodec(
|
codec: SseCodec(
|
||||||
decodeSuccessData: sse_decode_unit,
|
decodeSuccessData: sse_decode_unit,
|
||||||
@@ -2913,6 +2939,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
SecurityContext dco_decode_security_context(dynamic raw) {
|
||||||
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
|
final arr = raw as List<dynamic>;
|
||||||
|
if (arr.length != 4) throw Exception('unexpected arr length: expect 4 but see ${arr.length}');
|
||||||
|
return SecurityContext(
|
||||||
|
privateKey: dco_decode_String(arr[0]),
|
||||||
|
publicKey: dco_decode_String(arr[1]),
|
||||||
|
certificate: dco_decode_String(arr[2]),
|
||||||
|
certificateHash: dco_decode_String(arr[3]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
SessionEndReasonV2 dco_decode_session_end_reason_v_2(dynamic raw) {
|
SessionEndReasonV2 dco_decode_session_end_reason_v_2(dynamic raw) {
|
||||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
@@ -4126,6 +4165,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
SecurityContext sse_decode_security_context(SseDeserializer deserializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
var var_privateKey = sse_decode_String(deserializer);
|
||||||
|
var var_publicKey = sse_decode_String(deserializer);
|
||||||
|
var var_certificate = sse_decode_String(deserializer);
|
||||||
|
var var_certificateHash = sse_decode_String(deserializer);
|
||||||
|
return SecurityContext(privateKey: var_privateKey, publicKey: var_publicKey, certificate: var_certificate, certificateHash: var_certificateHash);
|
||||||
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
SessionEndReasonV2 sse_decode_session_end_reason_v_2(SseDeserializer deserializer) {
|
SessionEndReasonV2 sse_decode_session_end_reason_v_2(SseDeserializer deserializer) {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
@@ -5335,6 +5384,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_security_context(SecurityContext self, SseSerializer serializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
sse_encode_String(self.privateKey, serializer);
|
||||||
|
sse_encode_String(self.publicKey, serializer);
|
||||||
|
sse_encode_String(self.certificate, serializer);
|
||||||
|
sse_encode_String(self.certificateHash, serializer);
|
||||||
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_session_end_reason_v_2(SessionEndReasonV2 self, SseSerializer serializer) {
|
void sse_encode_session_end_reason_v_2(SessionEndReasonV2 self, SseSerializer serializer) {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
|||||||
@@ -435,6 +435,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
@protected
|
@protected
|
||||||
RTCStatus dco_decode_rtc_status(dynamic raw);
|
RTCStatus dco_decode_rtc_status(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
SecurityContext dco_decode_security_context(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
SessionEndReasonV2 dco_decode_session_end_reason_v_2(dynamic raw);
|
SessionEndReasonV2 dco_decode_session_end_reason_v_2(dynamic raw);
|
||||||
|
|
||||||
@@ -865,6 +868,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
@protected
|
@protected
|
||||||
RTCStatus sse_decode_rtc_status(SseDeserializer deserializer);
|
RTCStatus sse_decode_rtc_status(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
SecurityContext sse_decode_security_context(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
SessionEndReasonV2 sse_decode_session_end_reason_v_2(SseDeserializer deserializer);
|
SessionEndReasonV2 sse_decode_session_end_reason_v_2(SseDeserializer deserializer);
|
||||||
|
|
||||||
@@ -1341,6 +1347,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
@protected
|
@protected
|
||||||
void sse_encode_rtc_status(RTCStatus self, SseSerializer serializer);
|
void sse_encode_rtc_status(RTCStatus self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_security_context(SecurityContext self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_session_end_reason_v_2(SessionEndReasonV2 self, SseSerializer serializer);
|
void sse_encode_session_end_reason_v_2(SessionEndReasonV2 self, SseSerializer serializer);
|
||||||
|
|
||||||
|
|||||||
@@ -437,6 +437,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
@protected
|
@protected
|
||||||
RTCStatus dco_decode_rtc_status(dynamic raw);
|
RTCStatus dco_decode_rtc_status(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
SecurityContext dco_decode_security_context(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
SessionEndReasonV2 dco_decode_session_end_reason_v_2(dynamic raw);
|
SessionEndReasonV2 dco_decode_session_end_reason_v_2(dynamic raw);
|
||||||
|
|
||||||
@@ -867,6 +870,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
@protected
|
@protected
|
||||||
RTCStatus sse_decode_rtc_status(SseDeserializer deserializer);
|
RTCStatus sse_decode_rtc_status(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
SecurityContext sse_decode_security_context(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
SessionEndReasonV2 sse_decode_session_end_reason_v_2(SseDeserializer deserializer);
|
SessionEndReasonV2 sse_decode_session_end_reason_v_2(SseDeserializer deserializer);
|
||||||
|
|
||||||
@@ -1343,6 +1349,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
@protected
|
@protected
|
||||||
void sse_encode_rtc_status(RTCStatus self, SseSerializer serializer);
|
void sse_encode_rtc_status(RTCStatus self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_security_context(SecurityContext self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_session_end_reason_v_2(SessionEndReasonV2 self, SseSerializer serializer);
|
void sse_encode_session_end_reason_v_2(SessionEndReasonV2 self, SseSerializer serializer);
|
||||||
|
|
||||||
|
|||||||
+37
-3
@@ -255,6 +255,15 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bit-vec"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.3.2"
|
version = "1.3.2"
|
||||||
@@ -666,7 +675,7 @@ dependencies = [
|
|||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
"rand 0.9.5",
|
"rand 0.9.5",
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
"rcgen",
|
"rcgen 0.13.2",
|
||||||
"ring",
|
"ring",
|
||||||
"rustls",
|
"rustls",
|
||||||
"sec1",
|
"sec1",
|
||||||
@@ -1477,6 +1486,7 @@ dependencies = [
|
|||||||
"pem",
|
"pem",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"rand 0.9.5",
|
"rand 0.9.5",
|
||||||
|
"rcgen 0.14.8",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rsa",
|
"rsa",
|
||||||
"rustls",
|
"rustls",
|
||||||
@@ -2005,7 +2015,21 @@ dependencies = [
|
|||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"time",
|
"time",
|
||||||
"x509-parser 0.16.0",
|
"x509-parser 0.16.0",
|
||||||
"yasna",
|
"yasna 0.5.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rcgen"
|
||||||
|
version = "0.14.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "57f6d249aad744e274e682777a50283a225a32705394ee6d5fcc01efa25e4055"
|
||||||
|
dependencies = [
|
||||||
|
"pem",
|
||||||
|
"ring",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"time",
|
||||||
|
"x509-parser 0.18.1",
|
||||||
|
"yasna 0.6.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3194,7 +3218,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
"rand 0.9.5",
|
"rand 0.9.5",
|
||||||
"rcgen",
|
"rcgen 0.13.2",
|
||||||
"regex",
|
"regex",
|
||||||
"ring",
|
"ring",
|
||||||
"rtcp",
|
"rtcp",
|
||||||
@@ -3630,6 +3654,16 @@ dependencies = [
|
|||||||
"time",
|
"time",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yasna"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282"
|
||||||
|
dependencies = [
|
||||||
|
"bit-vec",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yoke"
|
name = "yoke"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
|
|||||||
@@ -20,6 +20,26 @@ pub struct KeyPair {
|
|||||||
pub public_key: String,
|
pub public_key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generates a new device identity: an RSA-2048 key pair and a self-signed
|
||||||
|
/// certificate whose SHA-256 fingerprint identifies the device.
|
||||||
|
pub fn generate_security_context() -> anyhow::Result<SecurityContext> {
|
||||||
|
let cert = localsend::crypto::cert::generate_self_signed()?;
|
||||||
|
|
||||||
|
Ok(SecurityContext {
|
||||||
|
private_key: cert.private_key_pem,
|
||||||
|
public_key: cert.public_key_pem,
|
||||||
|
certificate: cert.certificate_pem,
|
||||||
|
certificate_hash: cert.fingerprint,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SecurityContext {
|
||||||
|
pub private_key: String,
|
||||||
|
pub public_key: String,
|
||||||
|
pub certificate: String,
|
||||||
|
pub certificate_hash: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// Computes the SHA-256 checksum of a file, encoded as lowercase hex.
|
/// Computes the SHA-256 checksum of a file, encoded as lowercase hex.
|
||||||
///
|
///
|
||||||
/// The file is read chunk by chunk, so it is never fully loaded into memory.
|
/// The file is read chunk by chunk, so it is never fully loaded into memory.
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
|
|||||||
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
||||||
);
|
);
|
||||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
|
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
|
||||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1795427439;
|
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -286605518;
|
||||||
|
|
||||||
// Section: executor
|
// Section: executor
|
||||||
|
|
||||||
@@ -2638,6 +2638,40 @@ fn wire__crate__api__crypto__generate_key_pair_impl(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
fn wire__crate__api__crypto__generate_security_context_impl(
|
||||||
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
rust_vec_len_: i32,
|
||||||
|
data_len_: i32,
|
||||||
|
) {
|
||||||
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::SseCodec, _, _>(
|
||||||
|
flutter_rust_bridge::for_generated::TaskInfo {
|
||||||
|
debug_name: "generate_security_context",
|
||||||
|
port: Some(port_),
|
||||||
|
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||||
|
},
|
||||||
|
move || {
|
||||||
|
let message = unsafe {
|
||||||
|
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||||
|
ptr_,
|
||||||
|
rust_vec_len_,
|
||||||
|
data_len_,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut deserializer =
|
||||||
|
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
|
deserializer.end();
|
||||||
|
move |context| {
|
||||||
|
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>(
|
||||||
|
(move || {
|
||||||
|
let output_ok = crate::api::crypto::generate_security_context()?;
|
||||||
|
Ok(output_ok)
|
||||||
|
})(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
fn wire__crate__api__crypto__hash_file_impl(
|
fn wire__crate__api__crypto__hash_file_impl(
|
||||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
@@ -4262,6 +4296,22 @@ impl SseDecode for crate::api::webrtc::RTCStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SseDecode for crate::api::crypto::SecurityContext {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
|
let mut var_privateKey = <String>::sse_decode(deserializer);
|
||||||
|
let mut var_publicKey = <String>::sse_decode(deserializer);
|
||||||
|
let mut var_certificate = <String>::sse_decode(deserializer);
|
||||||
|
let mut var_certificateHash = <String>::sse_decode(deserializer);
|
||||||
|
return crate::api::crypto::SecurityContext {
|
||||||
|
private_key: var_privateKey,
|
||||||
|
public_key: var_publicKey,
|
||||||
|
certificate: var_certificate,
|
||||||
|
certificate_hash: var_certificateHash,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SseDecode for crate::api::server::SessionEndReasonV2 {
|
impl SseDecode for crate::api::server::SessionEndReasonV2 {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
@@ -4622,10 +4672,16 @@ fn pde_ffi_dispatcher_primary_impl(
|
|||||||
wire__crate__api__logging__enable_debug_logging_impl(port, ptr, rust_vec_len, data_len)
|
wire__crate__api__logging__enable_debug_logging_impl(port, ptr, rust_vec_len, data_len)
|
||||||
}
|
}
|
||||||
44 => wire__crate__api__crypto__generate_key_pair_impl(port, ptr, rust_vec_len, data_len),
|
44 => wire__crate__api__crypto__generate_key_pair_impl(port, ptr, rust_vec_len, data_len),
|
||||||
45 => wire__crate__api__crypto__hash_file_impl(port, ptr, rust_vec_len, data_len),
|
45 => wire__crate__api__crypto__generate_security_context_impl(
|
||||||
46 => wire__crate__api__multicast__start_multicast_impl(port, ptr, rust_vec_len, data_len),
|
port,
|
||||||
47 => wire__crate__api__server__start_server_impl(port, ptr, rust_vec_len, data_len),
|
ptr,
|
||||||
48 => wire__crate__api__crypto__verify_cert_impl(port, ptr, rust_vec_len, data_len),
|
rust_vec_len,
|
||||||
|
data_len,
|
||||||
|
),
|
||||||
|
46 => wire__crate__api__crypto__hash_file_impl(port, ptr, rust_vec_len, data_len),
|
||||||
|
47 => wire__crate__api__multicast__start_multicast_impl(port, ptr, rust_vec_len, data_len),
|
||||||
|
48 => wire__crate__api__server__start_server_impl(port, ptr, rust_vec_len, data_len),
|
||||||
|
49 => wire__crate__api__crypto__verify_cert_impl(port, ptr, rust_vec_len, data_len),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5516,6 +5572,29 @@ impl flutter_rust_bridge::IntoIntoDart<FrbWrapper<crate::api::webrtc::RTCStatus>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
|
impl flutter_rust_bridge::IntoDart for crate::api::crypto::SecurityContext {
|
||||||
|
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
||||||
|
[
|
||||||
|
self.private_key.into_into_dart().into_dart(),
|
||||||
|
self.public_key.into_into_dart().into_dart(),
|
||||||
|
self.certificate.into_into_dart().into_dart(),
|
||||||
|
self.certificate_hash.into_into_dart().into_dart(),
|
||||||
|
]
|
||||||
|
.into_dart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive
|
||||||
|
for crate::api::crypto::SecurityContext
|
||||||
|
{
|
||||||
|
}
|
||||||
|
impl flutter_rust_bridge::IntoIntoDart<crate::api::crypto::SecurityContext>
|
||||||
|
for crate::api::crypto::SecurityContext
|
||||||
|
{
|
||||||
|
fn into_into_dart(self) -> crate::api::crypto::SecurityContext {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
impl flutter_rust_bridge::IntoDart for FrbWrapper<crate::api::server::SessionEndReasonV2> {
|
impl flutter_rust_bridge::IntoDart for FrbWrapper<crate::api::server::SessionEndReasonV2> {
|
||||||
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
||||||
match self.0 {
|
match self.0 {
|
||||||
@@ -6669,6 +6748,16 @@ impl SseEncode for crate::api::webrtc::RTCStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SseEncode for crate::api::crypto::SecurityContext {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
|
<String>::sse_encode(self.private_key, serializer);
|
||||||
|
<String>::sse_encode(self.public_key, serializer);
|
||||||
|
<String>::sse_encode(self.certificate, serializer);
|
||||||
|
<String>::sse_encode(self.certificate_hash, serializer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SseEncode for crate::api::server::SessionEndReasonV2 {
|
impl SseEncode for crate::api::server::SessionEndReasonV2 {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
|
|||||||
Reference in New Issue
Block a user