This commit is contained in:
wanghongenpin
2024-07-28 20:50:15 +08:00
parent caa21fa192
commit d07a28b475
8 changed files with 436 additions and 50 deletions

View File

@@ -20,8 +20,10 @@ import 'dart:math';
import 'dart:typed_data';
import 'package:basic_utils/basic_utils.dart';
import 'package:network_proxy/network/util/x509.dart';
import 'package:network_proxy/network/util/x509/basic_constraints.dart';
import 'package:network_proxy/network/util/x509/x509.dart';
import 'package:path_provider/path_provider.dart';
import 'package:network_proxy/network/util/x509/key_usage.dart' as x509;
import 'file_read.dart';
@@ -95,10 +97,7 @@ class CertificateManager {
x509Subject['CN'] = host;
var csrPem = X509Generate.generateSelfSignedCertificate(caRoot, serverPubKey, caPriKey, 365,
extKeyUsage: [ExtendedKeyUsage.SERVER_AUTH],
sans: [host],
serialNumber: Random().nextInt(1000000).toString(),
subject: x509Subject);
sans: [host], serialNumber: Random().nextInt(1000000).toString(), subject: x509Subject);
return csrPem;
}
@@ -122,7 +121,18 @@ class CertificateManager {
};
x509Subject['CN'] = 'ProxyPin CA (${Platform.localHostname})';
var csrPem = generate(_caCert, serverPubKey, serverPriKey, 'ProxyPin CA (${Platform.localHostname})');
var csrPem = X509Generate.generateSelfSignedCertificate(
_caCert,
serverPubKey,
serverPriKey,
825,
sans: [x509Subject['CN']!],
serialNumber: Random().nextInt(1000000).toString(),
subject: x509Subject,
keyUsage: x509.KeyUsage(x509.KeyUsage.keyCertSign | x509.KeyUsage.cRLSign),
extKeyUsage: [ExtendedKeyUsage.SERVER_AUTH, ExtendedKeyUsage.CLIENT_AUTH],
basicConstraints: BasicConstraints(isCA: true),
);
//重新写入根证书
var caFile = await certificateFile();

View File

@@ -0,0 +1,9 @@
/// @author wanghongen
/// 2024/7/28
class BasicConstraints {
final bool isCA;
final int? pathLenConstraint;
final bool critical;
BasicConstraints({required this.isCA, this.pathLenConstraint, this.critical = true});
}

View File

@@ -0,0 +1,23 @@
import 'package:pointycastle/pointycastle.dart';
/// an object for the elements in the X.509 V3 extension block.
class Extension {
/// Key Usage
static final ASN1ObjectIdentifier keyUsage = ASN1ObjectIdentifier.fromIdentifierString("2.5.29.15");
/// Subject Alternative Name
static final ASN1ObjectIdentifier subjectAlternativeName = ASN1ObjectIdentifier.fromIdentifierString("2.5.29.17");
/// Basic Constraints
static final ASN1ObjectIdentifier basicConstraints = ASN1ObjectIdentifier.fromIdentifierString("2.5.29.19");
/// Extended Key Usage
static final ASN1ObjectIdentifier extendedKeyUsage = ASN1ObjectIdentifier.fromIdentifierString("2.5.29.37");
final ASN1ObjectIdentifier extnId;
final bool critical;
final ASN1OctetString value;
Extension(this.extnId, this.critical, this.value);
}

View File

@@ -0,0 +1,39 @@
import 'dart:typed_data';
import 'package:pointycastle/pointycastle.dart';
class KeyUsage {
static const int digitalSignature = (1 << 7);
static const int nonRepudiation = (1 << 6);
static const int keyEncipherment = (1 << 5);
static const int dataEncipherment = (1 << 4);
static const int keyAgreement = (1 << 3);
static const int keyCertSign = (1 << 2);
static const int cRLSign = (1 << 1);
static const int encipherOnly = (1 << 0);
static const int decipherOnly = (1 << 15);
final ASN1BitString bitString;
final bool critical;
KeyUsage(int usage, {this.critical = true}) : bitString = ASN1BitString.fromBytes(keyUsageBytes(usage));
static Uint8List keyUsageBytes(int valueBytes) {
var bytes = [valueBytes];
if (valueBytes > 0xFF) {
final int firstValueByte = (valueBytes & int.parse("ff00", radix: 16)) >> 8;
final int secondValueByte = (valueBytes & int.parse("00ff", radix: 16));
bytes = [firstValueByte, secondValueByte];
}
return Uint8List.fromList(<int>[
// BitString identifier
3,
// Length
bytes.length + 1,
// Unused bytes at the end
1,
...bytes
]);
}
}

View File

@@ -4,9 +4,13 @@ import 'dart:convert';
import 'dart:typed_data';
import 'package:basic_utils/basic_utils.dart';
import 'package:network_proxy/network/util/x509/extension.dart';
import 'package:network_proxy/network/util/x509/key_usage.dart' as x509;
import 'package:pointycastle/asn1/unsupported_object_identifier_exception.dart';
import 'package:pointycastle/pointycastle.dart';
import 'basic_constraints.dart';
/// @author wanghongen
/// 2023/7/26
class X509Generate {
@@ -38,7 +42,9 @@ class X509Generate {
String serialNumber = '1',
Map<String, String>? issuer,
Map<String, String>? subject,
x509.KeyUsage? keyUsage,
List<ExtendedKeyUsage>? extKeyUsage,
BasicConstraints? basicConstraints,
}) {
var data = ASN1Sequence();
@@ -87,18 +93,34 @@ class X509Generate {
data.add(_makePublicKeyBlock(publicKey));
// Add Extensions
if (IterableUtils.isNotNullOrEmpty(sans) || IterableUtils.isNotNullOrEmpty(extKeyUsage)) {
if (IterableUtils.isNotNullOrEmpty(sans) || keyUsage != null || IterableUtils.isNotNullOrEmpty(extKeyUsage)) {
var extensionTopSequence = ASN1Sequence();
// Add Key Usage
var extKeyUsageSequence = extKeyEncodings(extKeyUsage);
if (extKeyUsageSequence != null) {
extensionTopSequence.add(extKeyUsageSequence);
// Add basic constraints 2.5.29.19
if (basicConstraints != null) {
var basicConstraintsValue = ASN1Sequence();
basicConstraintsValue.add(ASN1Boolean(basicConstraints.isCA));
if (basicConstraints.pathLenConstraint != null) {
basicConstraintsValue.add(ASN1Integer(BigInt.from(basicConstraints.pathLenConstraint!)));
}
var octetString = ASN1OctetString(octets: basicConstraintsValue.encode());
var basicConstraintsSequence = ASN1Sequence();
basicConstraintsSequence.add(Extension.basicConstraints);
if (basicConstraints.critical) {
basicConstraintsSequence.add(ASN1Boolean(true));
}
basicConstraintsSequence.add(octetString);
extensionTopSequence.add(basicConstraintsSequence);
}
if (IterableUtils.isNotNullOrEmpty(sans)) {
var extensionTopSequence = ASN1Sequence();
// Add key usage 2.5.29.15
if (keyUsage != null) {
extensionTopSequence.add(keyUsageSequence(keyUsage)!);
}
//2.5.29.17
if (IterableUtils.isNotNullOrEmpty(sans)) {
var sanList = ASN1Sequence();
for (var s in sans!) {
sanList.add(ASN1PrintableString(stringValue: s, tag: 0x82));
@@ -106,15 +128,21 @@ class X509Generate {
var octetString = ASN1OctetString(octets: sanList.encode());
var sanSequence = ASN1Sequence();
sanSequence.add(ASN1ObjectIdentifier.fromIdentifierString('2.5.29.17'));
sanSequence.add(Extension.subjectAlternativeName);
sanSequence.add(octetString);
extensionTopSequence.add(sanSequence);
var extObj = ASN1Object(tag: 0xA3);
extObj.valueBytes = extensionTopSequence.encode();
data.add(extObj);
}
// Add ext key usage 2.5.29.37
var extKeyUsageSequence = extendedKeyUsageEncodings(extKeyUsage);
if (extKeyUsageSequence != null) {
extensionTopSequence.add(extKeyUsageSequence);
}
var extObj = ASN1Object(tag: 0xA3);
extObj.valueBytes = extensionTopSequence.encode();
data.add(extObj);
}
var outer = ASN1Sequence();
@@ -128,7 +156,20 @@ class X509Generate {
return '$BEGIN_CERT\n${chunks.join('\r\n')}\n$END_CERT';
}
static ASN1Sequence? extKeyEncodings(List<ExtendedKeyUsage>? extKeyUsage) {
static ASN1Sequence? keyUsageSequence(x509.KeyUsage keyUsages) {
var octetString = ASN1OctetString(octets: keyUsages.bitString.encode());
var keyUsageSequence = ASN1Sequence();
keyUsageSequence.add(Extension.keyUsage);
if (keyUsages.critical) {
keyUsageSequence.add(ASN1Boolean(true));
}
keyUsageSequence.add(octetString);
return keyUsageSequence;
}
static ASN1Sequence? extendedKeyUsageEncodings(List<ExtendedKeyUsage>? extKeyUsage) {
if (IterableUtils.isNullOrEmpty(extKeyUsage)) {
return null;
}
@@ -165,7 +206,7 @@ class X509Generate {
var octetString = ASN1OctetString(octets: extKeyUsageList.encode());
var extKeyUsageSequence = ASN1Sequence();
extKeyUsageSequence.add(ASN1ObjectIdentifier.fromIdentifierString('2.5.29.37'));
extKeyUsageSequence.add(Extension.extendedKeyUsage);
extKeyUsageSequence.add(octetString);
return extKeyUsageSequence;
}

View File

@@ -92,6 +92,7 @@ class _SslState extends State<SslWidget> {
child: Text(localizations.exportCaP12, style: const TextStyle(fontSize: 14))),
onPressed: () async {
//show p12 password
String? password;
showDialog(
context: context,
builder: (BuildContext context) {
@@ -101,11 +102,11 @@ class _SslState extends State<SslWidget> {
Padding(
padding: const EdgeInsets.all(10),
child: TextField(
controller: TextEditingController(),
decoration: const InputDecoration(
hintText: "Enter a password to protect p12 file",
border: OutlineInputBorder(),
),
onChanged: (val) => password = val,
),
),
Row(mainAxisAlignment: MainAxisAlignment.end, children: [
@@ -115,8 +116,8 @@ class _SslState extends State<SslWidget> {
String? path = (await getSaveLocation(suggestedName: "ProxyPinPkcs12.p12"))?.path;
if (path == null) return;
var password = TextEditingController().text;
var p12Bytes = await CertificateManager.generatePkcs12(password);
var p12Bytes = await CertificateManager.generatePkcs12(
password?.isNotEmpty == true ? password : null);
await File(path).writeAsBytes(p12Bytes);
if (context.mounted) Navigator.pop(context);
},

View File

@@ -1,7 +1,10 @@
import 'dart:io';
import 'dart:math';
import 'package:basic_utils/basic_utils.dart';
import 'package:network_proxy/network/util/x509.dart';
import 'package:network_proxy/network/util/x509/basic_constraints.dart';
import 'package:network_proxy/network/util/x509/key_usage.dart' as x509;
import 'package:network_proxy/network/util/x509/x509.dart';
void main() async {
var caPem = await File('assets/certs/ca.crt').readAsString();
@@ -11,33 +14,55 @@ void main() async {
var serverPubKey = generateRSAKeyPair.publicKey as RSAPublicKey;
var serverPriKey = generateRSAKeyPair.privateKey as RSAPrivateKey;
print(CryptoUtils.encodeRSAPublicKeyToPem(serverPubKey));
//保存私钥
var serverPriKeyPem = CryptoUtils.encodeRSAPrivateKeyToPem(serverPriKey);
print(serverPriKeyPem);
await File('assets/certs/server.key').writeAsString(serverPriKeyPem);
var rsaPrivateKeyFromPem = CryptoUtils.rsaPrivateKeyFromPem(serverPriKeyPem);
print(rsaPrivateKeyFromPem);
var crt = generate(caRoot, serverPubKey, serverPriKey);
var serverPublicKeyPem = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqVXqbCErPZMS+2Eb3MUT
eTNIYZHoCMZk5gFIo3pD70dZimQj2yMBIh9Rq4rO0/Dj9zt52vR1zbxDnmx/5TDC
djDHk/zHYW66VLYCo4n1H4/dddFvJ8Y8syBNpa+seSAR6ljF807gZqINGeNKi8Du
N82XiED2Ix3woE1jMQfP3E16alxHaejFBZ77SUOXJhJDM5SKD2H0bxGw9cVw9K69
NmnZMIM9+U8+TuM9EzvMUuHTY278Ov72c9HpO5OAx2zfyXGlmUGgyUCiYnxeATX5
LceGVEoT2MWhFibWvPBpH315xNXU57dWKWW714tPsvzzNHzKZspz/LQ36fU9goUg
NQIDAQAB
-----END PUBLIC KEY-----
""";
print(serverPublicKeyPem);
var readAsString = await File('assets/certs/server.key').readAsString();
// var rsaPrivateKeyFromPem = CryptoUtils.rsaPrivateKeyFromPem(serverPriKeyPem);
// print(rsaPrivateKeyFromPem);
var crt = generate(
caRoot, CryptoUtils.rsaPublicKeyFromPem(serverPublicKeyPem), CryptoUtils.rsaPrivateKeyFromPem(readAsString));
print(crt);
await File('assets/certs/server.crt').writeAsString(crt);
//TLS服务器证书必须包含ExtendedKeyUsageEKU扩展该扩展包含id-kp-serverAuth OID。
X509Utils.generateSelfSignedCertificate(serverPriKey, CryptoUtils.encodeRSAPublicKeyToPem(serverPubKey), 825,
serialNumber: Random().nextInt(1000000).toString(),
issuer: {
'C': 'CN',
'ST': 'BJ',
'L': 'Beijing',
'O': 'Proxy',
'OU': 'ProxyPin',
'CN': 'ProxyPin CA (${Platform.localHostname})'
},
extKeyUsage: [
ExtendedKeyUsage.SERVER_AUTH
]);
// await File('assets/certs/server.crt').writeAsString(crt);
// var readAsString2 = File('assets/certs/server.crt').readAsStringSync();
var generatePkcs12 = Pkcs12Utils.generatePkcs12(serverPriKeyPem, [crt], password: '123456');
await File('C:\\Users\\wanghongen\\Downloads\\server.p12').writeAsBytes(generatePkcs12);
//TLS服务器证书必须包含ExtendedKeyUsageEKU扩展该扩展包含id-kp-serverAuth OID。
// X509Utils.generateSelfSignedCertificate(serverPriKey, caPem, 825,
// serialNumber: Random().nextInt(1000000).toString(),
// sans: [
// 'ProxyPin CA (${Platform.localHostname})'
// ],
// issuer: {
// 'C': 'CN',
// 'ST': 'BJ',
// 'L': 'Beijing',
// 'O': 'Proxy',
// 'OU': 'ProxyPin',
// 'CN': 'ProxyPin CA (${Platform.localHostname})'
// },
// keyUsage: [
// KeyUsage.DIGITAL_SIGNATURE,
// KeyUsage.KEY_CERT_SIGN,
// KeyUsage.CRL_SIGN
// ],
// extKeyUsage: [
// ExtendedKeyUsage.SERVER_AUTH
// ]);
var generatePkcs12 = Pkcs12Utils.generatePkcs12(readAsString, [crt], password: '123');
await File('/Users/wanghongen/Downloads/server.p12').writeAsBytes(generatePkcs12);
}
/// 生成证书
@@ -50,10 +75,12 @@ String generate(X509CertificateData caRoot, RSAPublicKey serverPubKey, RSAPrivat
'O': 'Proxy',
'OU': 'ProxyPin',
};
x509Subject['CN'] = 'ProxyPin CA (${Platform.localHostname})';
x509Subject['CN'] = 'ProxyPin CA (wanghongen)';
var csrPem = X509Generate.generateSelfSignedCertificate(caRoot, serverPubKey, caPriKey, 825,
var csrPem = X509Generate.generateSelfSignedCertificate(caRoot, serverPubKey, caPriKey, 365,
keyUsage: x509.KeyUsage(x509.KeyUsage.keyCertSign | x509.KeyUsage.cRLSign),
extKeyUsage: [ExtendedKeyUsage.SERVER_AUTH],
basicConstraints: BasicConstraints(isCA: true),
sans: [x509Subject['CN']!],
serialNumber: Random().nextInt(1000000).toString(),
subject: x509Subject);

236
test/x509_test.dart Normal file
View File

@@ -0,0 +1,236 @@
import 'dart:typed_data';
import 'package:basic_utils/basic_utils.dart';
import 'package:network_proxy/network/util/x509/basic_constraints.dart';
import 'package:pointycastle/pointycastle.dart';
void main() {
encoding();
// Add ext key usage 2.5.29.37
// // Add key usage 2.5.29.15
// var keyUsage = [KeyUsage.KEY_CERT_SIGN, KeyUsage.CRL_SIGN];
//
// var encode = keyUsageSequence(keyUsage)?.encode();
// print(Int8List.view(encode!.buffer));
}
void encoding() {
var basicConstraints = BasicConstraints(isCA: true);
var extensionTopSequence = ASN1Sequence();
// Add basic constraints 2.5.29.19
var basicConstraintsValue = ASN1Sequence();
basicConstraintsValue.add(ASN1Boolean(basicConstraints.isCA));
if (basicConstraints.pathLenConstraint != null) {
basicConstraintsValue.add(ASN1Integer(BigInt.from(basicConstraints.pathLenConstraint!)));
}
var octetString = ASN1OctetString(octets: basicConstraintsValue.encode());
var basicConstraintsSequence = ASN1Sequence();
basicConstraintsSequence.add(ASN1ObjectIdentifier.fromIdentifierString('2.5.29.19'));
if (basicConstraints.critical) {
basicConstraintsSequence.add(ASN1Boolean(true));
}
basicConstraintsSequence.add(octetString);
extensionTopSequence.add(basicConstraintsSequence);
// Add key usage 2.5.29.15
var keyUsage = [KeyUsage.KEY_CERT_SIGN, KeyUsage.CRL_SIGN];
extensionTopSequence.add(keyUsageSequence(keyUsage)!);
//2.5.29.17
var sans = ['ProxyPin'];
if (IterableUtils.isNotNullOrEmpty(sans)) {
var sanList = ASN1Sequence();
for (var s in sans) {
sanList.add(ASN1PrintableString(stringValue: s, tag: 0x82));
}
var octetString = ASN1OctetString(octets: sanList.encode());
var sanSequence = ASN1Sequence();
sanSequence.add(ASN1ObjectIdentifier.fromIdentifierString('2.5.29.17'));
sanSequence.add(octetString);
extensionTopSequence.add(sanSequence);
}
// Add ext key usage 2.5.29.37
var extKeyUsage = [ExtendedKeyUsage.SERVER_AUTH];
var extKeyUsageSequence = extendedKeyUsageEncodings(extKeyUsage);
if (extKeyUsageSequence != null) {
extensionTopSequence.add(extKeyUsageSequence);
}
var extObj = ASN1Object(tag: 0xA3);
extObj.valueBytes = extensionTopSequence.encode();
print(Int8List.view(extensionTopSequence.encode().buffer));
// print(Int8List.view(extObj.encode().buffer));
}
void _basicConstraints() {
var basicConstraints = BasicConstraints(isCA: true);
var basicConstraintsValue = ASN1Sequence();
basicConstraintsValue.add(ASN1Boolean(basicConstraints.isCA));
if (basicConstraints.pathLenConstraint != null) {
basicConstraintsValue.add(ASN1Integer(BigInt.from(basicConstraints.pathLenConstraint!)));
}
print(Int8List.view(basicConstraintsValue.encode().buffer));
var octetString = ASN1OctetString(octets: basicConstraintsValue.encode());
print(Int8List.view(octetString.encode().buffer));
var basicConstraintsSequence = ASN1Sequence();
basicConstraintsSequence.add(ASN1ObjectIdentifier.fromIdentifierString('2.5.29.19'));
basicConstraintsSequence.add(ASN1Boolean(true));
basicConstraintsSequence.add(octetString);
print(Int8List.view(basicConstraintsSequence.encode().buffer));
//[48, 15, 6, 3, 85, 29, 19, 1, 1, -1, 4, 5, 48, 3, 1, 1, -1]
}
// class KeyUsage {
// static const int keyCertSign = (1 << 2);
// static const int cRLSign = (1 << 1);
//
// final ASN1BitString bitString;
//
// KeyUsage(int usage) : bitString = ASN1BitString(stringValues: getBytes(usage))..unusedbits = getPadBits(usage);
//
// static Uint8List getBytes(int bitString) {
// if (bitString == 0) {
// return Uint8List(0);
// }
//
// int bytes = 4;
// for (int i = 3; i >= 1; i--) {
// if ((bitString & (0xFF << (i * 8))) != 0) {
// break;
// }
// bytes--;
// }
//
// Uint8List result = Uint8List(bytes);
// for (int i = 0; i < bytes; i++) {
// result[i] = ((bitString >> (i * 8)) & 0xFF);
// }
//
// return result;
// }
//
// static int getPadBits(int bitString) {
// int val = 0;
// for (int i = 3; i >= 0; i--) {
// if (i != 0) {
// if ((bitString >> (i * 8)) != 0) {
// val = (bitString >> (i * 8)) & 0xFF;
// break;
// }
// } else {
// if (bitString != 0) {
// val = bitString & 0xFF;
// break;
// }
// }
// }
//
// if (val == 0) {
// return 0;
// }
//
// int bits = 1;
// while (((val <<= 1) & 0xFF) != 0) {
// bits++;
// }
//
// return 8 - bits;
// }
// }
ASN1Sequence? keyUsageSequence(List<KeyUsage>? keyUsages) {
int valueBytes = 0; // the last bit of the 2 bytes is always set
for (KeyUsage usage in keyUsages!) {
switch (usage) {
case KeyUsage.KEY_CERT_SIGN:
valueBytes |= (1 << 2);
break;
case KeyUsage.CRL_SIGN:
valueBytes |= (1 << 1);
break;
// Add other cases as needed
default:
throw Error();
}
}
var bytes = [valueBytes];
if (valueBytes > 0xFF) {
final int firstValueByte = (valueBytes & int.parse("ff00", radix: 16)) >> 8;
final int secondValueByte = (valueBytes & int.parse("00ff", radix: 16));
bytes = [firstValueByte, secondValueByte];
}
final Uint8List keyUsageBytes = Uint8List.fromList(<int>[
// BitString identifier
3,
// Length
bytes.length + 1,
// Unused bytes at the end
1,
...bytes
]);
print(keyUsageBytes);
var octetString = ASN1OctetString(octets: ASN1BitString.fromBytes(keyUsageBytes).encode());
var keyUsageSequence = ASN1Sequence();
keyUsageSequence.add(ASN1ObjectIdentifier.fromIdentifierString('2.5.29.15'));
keyUsageSequence.add(ASN1Boolean(true));
keyUsageSequence.add(octetString);
return keyUsageSequence;
}
ASN1Sequence? extendedKeyUsageEncodings(List<ExtendedKeyUsage>? extKeyUsage) {
if (IterableUtils.isNullOrEmpty(extKeyUsage)) {
return null;
}
var extKeyUsageList = ASN1Sequence();
for (var s in extKeyUsage!) {
var oi = <int>[];
switch (s) {
case ExtendedKeyUsage.SERVER_AUTH:
oi = [1, 3, 6, 1, 5, 5, 7, 3, 1];
break;
case ExtendedKeyUsage.CLIENT_AUTH:
oi = [1, 3, 6, 1, 5, 5, 7, 3, 2];
break;
case ExtendedKeyUsage.CODE_SIGNING:
oi = [1, 3, 6, 1, 5, 5, 7, 3, 3];
break;
case ExtendedKeyUsage.EMAIL_PROTECTION:
oi = [1, 3, 6, 1, 5, 5, 7, 3, 4];
break;
case ExtendedKeyUsage.TIME_STAMPING:
oi = [1, 3, 6, 1, 5, 5, 7, 3, 8];
break;
case ExtendedKeyUsage.OCSP_SIGNING:
oi = [1, 3, 6, 1, 5, 5, 7, 3, 9];
break;
case ExtendedKeyUsage.BIMI:
oi = [1, 3, 6, 1, 5, 5, 7, 3, 31];
break;
}
extKeyUsageList.add(ASN1ObjectIdentifier(oi));
}
var octetString = ASN1OctetString(octets: extKeyUsageList.encode());
var extKeyUsageSequence = ASN1Sequence();
extKeyUsageSequence.add(ASN1ObjectIdentifier.fromIdentifierString('2.5.29.37'));
extKeyUsageSequence.add(octetString);
return extKeyUsageSequence;
}