Refactor authentication mechanism, minor changes to CryptoPrivateKey files, implement Dss public Key

This commit is contained in:
olegkap_cp
2010-08-17 20:32:49 +00:00
parent ff27b12cf2
commit d5f5ee424c
19 changed files with 398 additions and 336 deletions
+1 -1
View File
@@ -131,7 +131,7 @@ namespace Renci.SshClient
throw new NotSupportedException(string.Format("Key '{0}' is not supported.", keyName));
}
this._key.Load(System.Convert.FromBase64String(data.ToString()), passPhrase.GetSshBytes());
this._key.Load(System.Convert.FromBase64String(data.ToString()), passPhrase);
}
public void Open(string fileName)
@@ -121,10 +121,11 @@
<Compile Include="KeyFile.cs" />
<Compile Include="Messages\Connection\ChannelMessage.cs" />
<Compile Include="Messages\Connection\RequestNames.cs" />
<Compile Include="Services\UserAuthentication.cs" />
<Compile Include="Services\UserAuthenticationHost.cs" />
<Compile Include="Services\UserAuthenticationPassword.cs" />
<Compile Include="Services\UserAuthenticationPublicKey.cs" />
<Compile Include="Security\UserAuthentication.cs" />
<Compile Include="Security\UserAuthenticationHost.cs" />
<Compile Include="Security\UserAuthenticationNone.cs" />
<Compile Include="Security\UserAuthenticationPassword.cs" />
<Compile Include="Security\UserAuthenticationPublicKey.cs" />
<Compile Include="Channels\Channel.cs" />
<Compile Include="Channels\ChannelExec.cs" />
<Compile Include="Channels\ChannelTypes.cs" />
@@ -189,9 +190,6 @@
</Compile>
<Compile Include="Messages\Authentication\SuccessMessage.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Services\ConnectionService.cs" />
<Compile Include="Services\Service.cs" />
<Compile Include="Services\UserAuthenticationService.cs" />
<Compile Include="Session.cs" />
<Compile Include="SessionSSHv2.cs" />
<Compile Include="Settings.cs" />
@@ -201,6 +199,7 @@
<Compile Include="Shell.cs" />
</ItemGroup>
<ItemGroup>
<Folder Include="Services\" />
<Folder Include="Shell\" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
@@ -10,7 +10,7 @@ namespace Renci.SshClient.Security
this.Load(data, null);
}
public abstract void Load(IEnumerable<byte> data, IEnumerable<byte> passPhrase);
public abstract void Load(IEnumerable<byte> data, string passPhrase);
public abstract CryptoPublicKey GetPublicKey();
@@ -19,7 +19,7 @@ namespace Renci.SshClient.Security
get { return "ssh-dss"; }
}
public override void Load(IEnumerable<byte> data, IEnumerable<byte> passPhrase)
public override void Load(IEnumerable<byte> data, string passPhrase)
{
if (passPhrase != null)
{
@@ -68,7 +68,7 @@ namespace Renci.SshClient.Security
public override CryptoPublicKey GetPublicKey()
{
return new CryptoPublicKeyDss();
return new CryptoPublicKeyDss(this._p, this._q, this._g, this._x);
}
public override IEnumerable<byte> GetSignature(IEnumerable<byte> key)
@@ -90,7 +90,7 @@ namespace Renci.SshClient.Security
var DSA = new System.Security.Cryptography.DSACryptoServiceProvider();
DSA.ImportParameters(DSAKeyInfo);
var DSAFormatter = new RSAPKCS1SignatureFormatter(DSA);
var DSAFormatter = new DSASignatureFormatter(DSA);
DSAFormatter.SetHashAlgorithm("SHA1");
var signature = DSAFormatter.CreateSignature(sha1);
@@ -22,7 +22,7 @@ namespace Renci.SshClient.Security
get { return "ssh-rsa"; }
}
public override void Load(IEnumerable<byte> data, IEnumerable<byte> passPhrase)
public override void Load(IEnumerable<byte> data, string passPhrase)
{
if (passPhrase != null)
{
@@ -92,14 +92,14 @@ namespace Renci.SshClient.Security
{
RSAParameters RSAKeyInfo = new RSAParameters();
RSAKeyInfo.Exponent = _exponent.TrimLeadinZero().ToArray();
RSAKeyInfo.D = _dValue.TrimLeadinZero().ToArray();
RSAKeyInfo.Modulus = _modulus.TrimLeadinZero().ToArray();
RSAKeyInfo.P = _pValue.TrimLeadinZero().ToArray();
RSAKeyInfo.Q = _qValue.TrimLeadinZero().ToArray();
RSAKeyInfo.DP = _dpValue.TrimLeadinZero().ToArray();
RSAKeyInfo.DQ = _dqValue.TrimLeadinZero().ToArray();
RSAKeyInfo.InverseQ = _inverseQ.TrimLeadinZero().ToArray();
RSAKeyInfo.Exponent = this._exponent.TrimLeadinZero().ToArray();
RSAKeyInfo.D = this._dValue.TrimLeadinZero().ToArray();
RSAKeyInfo.Modulus = this._modulus.TrimLeadinZero().ToArray();
RSAKeyInfo.P = this._pValue.TrimLeadinZero().ToArray();
RSAKeyInfo.Q = this._qValue.TrimLeadinZero().ToArray();
RSAKeyInfo.DP = this._dpValue.TrimLeadinZero().ToArray();
RSAKeyInfo.DQ = this._dqValue.TrimLeadinZero().ToArray();
RSAKeyInfo.InverseQ = this._inverseQ.TrimLeadinZero().ToArray();
cs.Write(data, 0, data.Length);
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using Renci.SshClient.Common;
namespace Renci.SshClient.Security
{
@@ -111,7 +112,38 @@ namespace Renci.SshClient.Security
public override IEnumerable<byte> GetBytes()
{
throw new NotImplementedException();
return new DsaPublicKeyData
{
P = this._p,
Q = this._q,
G = this._g,
Public = this._x,
}.GetBytes();
}
private class DsaPublicKeyData : SshData
{
public IEnumerable<byte> P { get; set; }
public IEnumerable<byte> Q { get; set; }
public IEnumerable<byte> G { get; set; }
public IEnumerable<byte> Public { get; set; }
protected override void LoadData()
{
}
protected override void SaveData()
{
this.Write("ssh-dss");
this.Write(this.P.GetSshString());
this.Write(this.Q.GetSshString());
this.Write(this.G.GetSshString());
this.Write(this.Public.GetSshString());
}
}
}
}
@@ -0,0 +1,65 @@
using Renci.SshClient.Messages;
using Renci.SshClient.Messages.Authentication;
namespace Renci.SshClient.Security
{
internal abstract class UserAuthentication
{
public abstract string Name { get; }
public bool IsAuthenticated { get; private set; }
public string ErrorMessage { get; private set; }
protected Session Session { get; private set; }
public UserAuthentication(Session session)
{
this.Session = session;
}
/// <summary>
/// Executes this instance.
/// </summary>
/// <returns>true if method was execute; otherwise false.</returns>
public bool Execute()
{
Message.RegisterMessageType<FailureMessage>(MessageTypes.UserAuthenticationFailure);
Message.RegisterMessageType<SuccessMessage>(MessageTypes.UserAuthenticationSuccess);
Message.RegisterMessageType<BannerMessage>(MessageTypes.UserAuthenticationBanner);
this.Session.MessageReceived += Session_MessageReceived;
var result = this.Run();
this.Session.MessageReceived -= Session_MessageReceived;
Message.UnRegisterMessageType(MessageTypes.UserAuthenticationFailure);
Message.UnRegisterMessageType(MessageTypes.UserAuthenticationSuccess);
Message.UnRegisterMessageType(MessageTypes.UserAuthenticationBanner);
return result;
}
protected abstract bool Run();
protected abstract void HandleMessage<T>(T message) where T : Message;
protected virtual void HandleMessage(SuccessMessage message)
{
this.IsAuthenticated = true;
}
protected virtual void HandleMessage(FailureMessage message)
{
this.ErrorMessage = message.Message;
this.IsAuthenticated = false;
}
private void Session_MessageReceived(object sender, Common.MessageReceivedEventArgs e)
{
dynamic message = e.Message;
this.HandleMessage(message);
}
}
}
@@ -1,4 +1,4 @@
namespace Renci.SshClient.Services
namespace Renci.SshClient.Security
{
internal class UserAuthenticationHost : UserAuthentication
{
@@ -15,7 +15,12 @@
}
public override bool Start()
protected override bool Run()
{
throw new System.NotImplementedException();
}
protected override void HandleMessage<T>(T message)
{
throw new System.NotImplementedException();
}
@@ -0,0 +1,51 @@
using System.Threading;
using Renci.SshClient.Messages;
using Renci.SshClient.Messages.Authentication;
namespace Renci.SshClient.Security
{
internal class UserAuthenticationNone : UserAuthentication
{
private EventWaitHandle _authenticationCompleted = new AutoResetEvent(false);
public override string Name
{
get { return "none"; }
}
public UserAuthenticationNone(Session session)
: base(session)
{
}
protected override bool Run()
{
this.Session.SendMessage(new RequestMessage
{
ServiceName = ServiceNames.Connection,
Username = this.Session.ConnectionInfo.Username,
});
this.Session.WaitHandle(this._authenticationCompleted);
return true;
}
protected override void HandleMessage<T>(T message)
{
}
protected override void HandleMessage(SuccessMessage message)
{
base.HandleMessage(message);
this._authenticationCompleted.Set();
}
protected override void HandleMessage(FailureMessage message)
{
base.HandleMessage(message);
this._authenticationCompleted.Set();
}
}
}
@@ -0,0 +1,62 @@
using System.Threading;
using Renci.SshClient.Messages;
using Renci.SshClient.Messages.Authentication;
namespace Renci.SshClient.Security
{
internal class UserAuthenticationPassword : UserAuthentication
{
private EventWaitHandle _authenticationCompleted = new AutoResetEvent(false);
public override string Name
{
get
{
return "password";
}
}
public UserAuthenticationPassword(Session session)
: base(session)
{
}
protected override bool Run()
{
// TODO: Handle all user authentication messages
//Message.RegisterMessageType<PasswordChangeRequiredMessage>(MessageTypes.UserAuthenticationPasswordChangeRequired);
if (string.IsNullOrEmpty(this.Session.ConnectionInfo.Password))
return false;
this.Session.SendMessage(new PasswordRequestMessage
{
ServiceName = ServiceNames.Connection,
Username = this.Session.ConnectionInfo.Username,
Password = this.Session.ConnectionInfo.Password,
});
this.Session.WaitHandle(this._authenticationCompleted);
return true;
}
protected override void HandleMessage<T>(T message)
{
// TODO: Handle password specific messages
}
protected override void HandleMessage(SuccessMessage message)
{
base.HandleMessage(message);
this._authenticationCompleted.Set();
}
protected override void HandleMessage(FailureMessage message)
{
base.HandleMessage(message);
this._authenticationCompleted.Set();
}
}
}
@@ -0,0 +1,109 @@
using System.Threading;
using Renci.SshClient.Common;
using Renci.SshClient.Messages;
using Renci.SshClient.Messages.Authentication;
namespace Renci.SshClient.Security
{
internal class UserAuthenticationPublicKey : UserAuthentication
{
private EventWaitHandle _authenticationCompleted = new AutoResetEvent(false);
public override string Name
{
get
{
return "publickey";
}
}
public UserAuthenticationPublicKey(Session session)
: base(session)
{
}
protected override bool Run()
{
if (this.Session.ConnectionInfo.KeyFile == null)
return false;
Message.RegisterMessageType<InformationRequestMessage>(MessageTypes.UserAuthenticationInformationRequest);
// TODO: Complete full public key implemention which includes other messages
var message = new PublicKeyRequestMessage
{
ServiceName = ServiceNames.Connection,
Username = this.Session.ConnectionInfo.Username,
PublicKeyAlgorithmName = this.Session.ConnectionInfo.KeyFile.AlgorithmName,
PublicKeyData = this.Session.ConnectionInfo.KeyFile.PublicKey,
Signature = new byte[] { },
//Signature = this.Session.ConnectionInfo.KeyFile.GetSignature(this.Session.SessionId),
};
var signatureData = new SignatureData(message, this.Session.SessionId.GetSshString()).GetBytes();
var signature = this.Session.ConnectionInfo.KeyFile.GetSignature(signatureData);
message.Signature = signature;
this.Session.SendMessage(message);
this.Session.WaitHandle(this._authenticationCompleted);
Message.UnRegisterMessageType(MessageTypes.UserAuthenticationInformationRequest);
return true;
}
protected override void HandleMessage<T>(T message)
{
throw new System.NotImplementedException();
}
protected override void HandleMessage(SuccessMessage message)
{
base.HandleMessage(message);
this._authenticationCompleted.Set();
}
protected override void HandleMessage(FailureMessage message)
{
base.HandleMessage(message);
this._authenticationCompleted.Set();
}
private class SignatureData : SshData
{
private PublicKeyRequestMessage _message;
private string _sessionId;
public SignatureData(PublicKeyRequestMessage message, string sessionId)
{
this._message = message;
this._sessionId = sessionId;
}
protected override void LoadData()
{
throw new System.NotImplementedException();
}
protected override void SaveData()
{
this.Write(this._sessionId);
this.Write((byte)this._message.MessageType);
this.Write(this._message.Username);
this.Write("ssh-connection");
this.Write("publickey");
this.Write((byte)1);
this.Write(this._message.PublicKeyAlgorithmName);
this.Write(this._message.PublicKeyData.GetSshString());
}
}
}
}
@@ -1,19 +0,0 @@
using System;
using Renci.SshClient.Messages;
namespace Renci.SshClient.Services
{
internal class ConnectionService : Service
{
public override ServiceNames ServiceName
{
get { throw new NotImplementedException(); }
}
public ConnectionService(Session session)
: base(session)
{
}
}
}
@@ -1,22 +0,0 @@
using Renci.SshClient.Messages;
namespace Renci.SshClient.Services
{
internal abstract class Service
{
public abstract ServiceNames ServiceName { get; }
protected Session Session { get; private set; }
public Service(Session session)
{
this.Session = session;
}
protected void SendMessage(Message message)
{
this.Session.SendMessage(message);
}
}
}
@@ -1,24 +0,0 @@
using Renci.SshClient.Messages;
namespace Renci.SshClient.Services
{
internal abstract class UserAuthentication
{
public abstract string Name { get; }
protected Session Session { get; private set; }
public UserAuthentication(Session session)
{
this.Session = session;
}
public abstract bool Start();
protected void SendMessage(Message message)
{
this.Session.SendMessage(message);
}
}
}
@@ -1,40 +0,0 @@
using Renci.SshClient.Messages;
using Renci.SshClient.Messages.Authentication;
namespace Renci.SshClient.Services
{
internal class UserAuthenticationPassword : UserAuthentication
{
public override string Name
{
get
{
return "password";
}
}
public UserAuthenticationPassword(Session session)
: base(session)
{
}
public override bool Start()
{
// TODO: Handle all user authentication messages
//Message.RegisterMessageType<PasswordChangeRequiredMessage>(MessageTypes.UserAuthenticationPasswordChangeRequired);
if (!string.IsNullOrEmpty(this.Session.ConnectionInfo.Password))
{
this.SendMessage(new PasswordRequestMessage
{
ServiceName = ServiceNames.Connection,
Username = this.Session.ConnectionInfo.Username,
Password = this.Session.ConnectionInfo.Password,
});
return true;
}
return false;
}
}
}
@@ -1,41 +0,0 @@
using Renci.SshClient.Messages;
using Renci.SshClient.Messages.Authentication;
namespace Renci.SshClient.Services
{
internal class UserAuthenticationPublicKey : UserAuthentication
{
public override string Name
{
get
{
return "publickey";
}
}
public UserAuthenticationPublicKey(Session session)
: base(session)
{
}
public override bool Start()
{
if (this.Session.ConnectionInfo.KeyFile != null)
{
// TODO: Complete full public key implemention which includes other messages
this.SendMessage(new PublicKeyRequestMessage
{
ServiceName = ServiceNames.Connection,
Username = this.Session.ConnectionInfo.Username,
PublicKeyAlgorithmName = this.Session.ConnectionInfo.KeyFile.AlgorithmName,
PublicKeyData = this.Session.ConnectionInfo.KeyFile.PublicKey,
Signature = this.Session.ConnectionInfo.KeyFile.GetSignature(this.Session.SessionId),
});
return true;
}
return false;
}
}
}
@@ -1,155 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Renci.SshClient.Common;
using Renci.SshClient.Messages;
using Renci.SshClient.Messages.Authentication;
using Renci.SshClient.Messages.Transport;
using Renci.SshClient.Security;
namespace Renci.SshClient.Services
{
internal class UserAuthenticationService : Service
{
private IList<string> _executedMethods = new List<string>();
private EventWaitHandle _serviceAccepted = new AutoResetEvent(false);
private EventWaitHandle _authenticationCompleted = new AutoResetEvent(false);
public override ServiceNames ServiceName
{
get { return ServiceNames.UserAuthentication; }
}
public EventWaitHandle AuthenticationCompletedHandle { get; private set; }
public string ErrorMessage { get; private set; }
public bool IsAuthenticated { get; private set; }
public UserAuthenticationService(Session session)
: base(session)
{
this.AuthenticationCompletedHandle = new AutoResetEvent(false);
}
public void AuthenticateUser()
{
// Register Authentication response messages
Message.RegisterMessageType<FailureMessage>(MessageTypes.UserAuthenticationFailure);
Message.RegisterMessageType<SuccessMessage>(MessageTypes.UserAuthenticationSuccess);
Message.RegisterMessageType<BannerMessage>(MessageTypes.UserAuthenticationBanner);
// Attach event handlers to handle messages
this.Session.MessageReceived += SessionInfo_MessageReceived;
// Request user authorization service
this.SendMessage(new ServiceRequestMessage
{
ServiceName = ServiceNames.UserAuthentication,
});
// Wait for service to be accepted
this.Session.WaitHandle(this._serviceAccepted);
// Start by quering supported authentication methods
this.SendMessage(new RequestMessage
{
Username = this.Session.ConnectionInfo.Username,
ServiceName = ServiceNames.Connection,
});
// Wait for authentication to be completed
this.Session.WaitHandle(this._authenticationCompleted);
this.Session.MessageReceived -= SessionInfo_MessageReceived;
Message.UnRegisterMessageType(MessageTypes.UserAuthenticationFailure);
Message.UnRegisterMessageType(MessageTypes.UserAuthenticationSuccess);
Message.UnRegisterMessageType(MessageTypes.UserAuthenticationBanner);
}
private void SessionInfo_MessageReceived(object sender, MessageReceivedEventArgs e)
{
this.HandleMessage((dynamic)e.Message);
}
private void HandleMessage<T>(T message)
{
// Ignore messages that cannot be handled by this module
}
private void HandleMessage(ServiceAcceptMessage message)
{
if (message.ServiceName == ServiceNames.UserAuthentication)
{
this._serviceAccepted.Set();
}
}
private void HandleMessage(SuccessMessage message)
{
this.AuthenticationSucceded();
}
private void HandleMessage(FailureMessage message)
{
if (message.PartialSuccess)
{
this.AuthenticationFailed(message.Message);
return;
}
// Get method that was not executed yet
var methodsToTry = message.AllowedAuthentications.Except(this._executedMethods);
if (methodsToTry.Count() == 0)
{
this.AuthenticationFailed(string.Format("User '{0}' cannot be authorized.", this.Session.ConnectionInfo.Username));
return;
}
// Execute authentication method
foreach (var methodName in methodsToTry)
{
UserAuthentication userAuthentication = null;
if (methodName == "publickey")
{
userAuthentication = new UserAuthenticationPublicKey(this.Session);
}
else if (methodName == "password")
{
userAuthentication = new UserAuthenticationPassword(this.Session);
}
this._executedMethods.Add(methodName);
if (userAuthentication != null)
{
if (userAuthentication.Start())
break;
}
}
}
private void HandleMessage(BannerMessage message)
{
}
private void AuthenticationFailed(string message)
{
this.IsAuthenticated = false;
this.ErrorMessage = message;
this._authenticationCompleted.Set();
}
private void AuthenticationSucceded()
{
this.IsAuthenticated = true;
this._authenticationCompleted.Set();
}
}
}
+42 -11
View File
@@ -5,6 +5,7 @@ using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
@@ -14,7 +15,6 @@ using Renci.SshClient.Messages;
using Renci.SshClient.Messages.Connection;
using Renci.SshClient.Messages.Transport;
using Renci.SshClient.Security;
using Renci.SshClient.Services;
namespace Renci.SshClient
{
@@ -59,19 +59,19 @@ namespace Renci.SshClient
private Socket _socket;
private int _waitTimeout = 5 * 1000; // Set default receive timeout to 5 seconds
private KeyExchange _keyExhcange;
private BackgroundWorker _messageListener;
private EventWaitHandle _keyExhangedFinishedWaitHandle = new AutoResetEvent(false);
private IDictionary<ServiceNames, Service> _services = new Dictionary<ServiceNames, Service>();
private EventWaitHandle _serviceAccepted = new AutoResetEvent(false);
private EventWaitHandle _exceptionWaitHandle = new AutoResetEvent(false);
private IDictionary<uint, uint> _openChannels = new Dictionary<uint, uint>();
private EventWaitHandle _exceptionWaitHandle = new AutoResetEvent(false);
private IDictionary<string, UserAuthentication> _executedAuthenticationMethods = new Dictionary<string, UserAuthentication>();
/// <summary>
/// Exception that need to be thrown by waiting thread
@@ -83,6 +83,11 @@ namespace Renci.SshClient
/// </summary>
private bool _isDisconnectByClient;
/// <summary>
/// Specifies weither connection is authenticated
/// </summary>
private bool _isAuthenticated;
public event EventHandler<MessageReceivedEventArgs> MessageReceived;
protected HMAC ServerMac { get; private set; }
@@ -195,13 +200,38 @@ namespace Renci.SshClient
return;
}
var authenticationService = new UserAuthenticationService(this);
authenticationService.AuthenticateUser();
if (!authenticationService.IsAuthenticated)
// Request user authorization service
this.SendMessage(new ServiceRequestMessage
{
throw new InvalidOperationException(string.Format("User cannot be authenticated. Reason: {0}.", authenticationService.ErrorMessage));
ServiceName = ServiceNames.UserAuthentication,
});
// Wait for service to be accepted
this.WaitHandle(this._serviceAccepted);
// This implemention will ignore supported by server methods and will try to authenticated user using method supported by the client.
string errorMessage = null; // Hold last authentication error if any
foreach (var methodName in Settings.SupportedAuthenticationMethods.Keys)
{
var userAuthentication = Settings.SupportedAuthenticationMethods[methodName](this);
if (userAuthentication.Execute())
{
if (userAuthentication.IsAuthenticated)
{
this._isAuthenticated = true;
break;
}
else
{
errorMessage = userAuthentication.ErrorMessage;
}
}
}
if (!this._isAuthenticated)
{
throw new AuthenticationException(errorMessage ?? "User cannot be authenticated.");
}
}
@@ -277,6 +307,7 @@ namespace Renci.SshClient
protected virtual void HandleMessage(ServiceAcceptMessage message)
{
this._serviceAccepted.Set();
}
protected virtual void HandleMessage(ServiceRequestMessage message)
@@ -16,6 +16,8 @@ namespace Renci.SshClient
public static IDictionary<string, Func<CryptoPublicKey>> HostKeyAlgorithms { get; private set; }
public static IDictionary<string, Func<Session, UserAuthentication>> SupportedAuthenticationMethods { get; private set; }
static Settings()
{
Settings.KeyExchangeAlgorithms = new Dictionary<string, Func<Session, KeyExchange>>()
@@ -43,6 +45,13 @@ namespace Renci.SshClient
{"ssh-rsa", () => { return new CryptoPublicKeyRsa();}},
{"ssh-dsa", () => { return new CryptoPublicKeyDss();}}, // TODO: Need to be tested
};
Settings.SupportedAuthenticationMethods = new Dictionary<string, Func<Session, UserAuthentication>>()
{
{"none", (session)=> {return new UserAuthenticationNone(session);}},
{"publickey", (session)=> {return new UserAuthenticationPublicKey(session);}},
{"password", (session)=> {return new UserAuthenticationPassword(session);}},
};
}
}
}