/// <summary>
/// Answers the message.
/// </summary>
/// <param name='messageStream'>Message stream.</param>
/// <param name="process">The calling process or <c>null</c> if the process
/// could not be obtained.</param>
/// <remarks>code based on winpgnt.c from PuTTY source code</remarks>
public void AnswerMessage(Stream messageStream, Process process = null)
{
if (messageStream.CanTimeout) {
messageStream.ReadTimeout = 5000;
}
var messageParser = new BlobParser(messageStream);
var responseBuilder = new BlobBuilder();
BlobHeader header;
try {
header = messageParser.ReadHeader();
if (MessageReceived != null) {
var eventArgs = new MessageReceivedEventArgs(header);
MessageReceived(this, eventArgs);
if (eventArgs.Fail) {
throw new Exception ();
}
}
} catch (Exception) {
header = new BlobHeader();
header.Message = Message.UNKNOWN;
// this will cause the switch statement below to use the default case
// which returns an error to the stream.
}
switch (header.Message) {
case Message.SSH1_AGENTC_REQUEST_RSA_IDENTITIES:
/*
* Reply with SSH1_AGENT_RSA_IDENTITIES_ANSWER.
*/
try {
if (header.BlobLength > 1) {
// ruby net-ssh tries to send a SSH2_AGENT_REQUEST_VERSION message
// which has the same id number as SSH1_AGENTC_REQUEST_RSA_IDENTITIES
// with a string tacked on. We need to read the string from the
// stream, but it is not used for anything.
messageParser.ReadString ();
}
var keyList = ListKeys(SshVersion.SSH1);
if (FilterKeyListCallback != null) {
keyList = FilterKeyListCallback(keyList);
}
foreach (SshKey key in keyList) {
responseBuilder.AddBytes(key.GetPublicKeyBlob());
responseBuilder.AddStringBlob(key.Comment);
}
responseBuilder.InsertHeader(Message.SSH1_AGENT_RSA_IDENTITIES_ANSWER,
keyList.Count);
// TODO may want to check that there is enough room in the message stream
break; // succeeded
} catch (Exception ex) {
Debug.Fail(ex.ToString());
}
goto default; // failed
case Message.SSH2_AGENTC_REQUEST_IDENTITIES:
/*
* Reply with SSH2_AGENT_IDENTITIES_ANSWER.
*/
try {
var keyList = ListKeys(SshVersion.SSH2);
if (FilterKeyListCallback != null) {
keyList = FilterKeyListCallback(keyList);
}
foreach (SshKey key in keyList) {
responseBuilder.AddBlob(key.GetPublicKeyBlob());
responseBuilder.AddStringBlob(key.Comment);
}
responseBuilder.InsertHeader(Message.SSH2_AGENT_IDENTITIES_ANSWER,
keyList.Count);
// TODO may want to check that there is enough room in the message stream
break; // succeeded
} catch (Exception ex) {
Debug.Fail(ex.ToString());
}
goto default; // failed
case Message.SSH1_AGENTC_RSA_CHALLENGE:
/*
* Reply with either SSH1_AGENT_RSA_RESPONSE or
* SSH_AGENT_FAILURE, depending on whether we have that key
* or not.
*/
try {
//Reading publicKey information
var publicKeyParams = messageParser.ReadSsh1PublicKeyData(true);
//Searching for Key here
var matchingKey = mKeyList.Where(key => key.Version == SshVersion.SSH1
&& (key.GetPublicKeyParameters().Equals(publicKeyParams))).Single();
//Reading challenge
var encryptedChallenge = messageParser.ReadSsh1BigIntBlob();
var sessionId = messageParser.ReadBytes(16);
//Checking responseType field
if (messageParser.ReadInt() != 1) {
goto default; //responseType !=1 is not longer supported
}
//Answering to the challenge
var engine = new Pkcs1Encoding(new RsaEngine());
engine.Init(false /* decrypt */, matchingKey.GetPrivateKeyParameters());
var decryptedChallenge = engine.ProcessBlock(encryptedChallenge,
0, encryptedChallenge.Length);
using (MD5 md5 = MD5.Create()) {
var md5Buffer = new byte[48];
decryptedChallenge.CopyTo(md5Buffer, 0);
sessionId.CopyTo(md5Buffer, 32);
responseBuilder.AddBytes(md5.ComputeHash(md5Buffer));
responseBuilder.InsertHeader(Message.SSH1_AGENT_RSA_RESPONSE);
break;
}
} catch (InvalidOperationException) {
// this is expected if there is not a matching key
} catch (Exception ex) {
Debug.Fail(ex.ToString());
}
goto default; // failed
case Message.SSH2_AGENTC_SIGN_REQUEST:
/*
* Reply with either SSH2_AGENT_SIGN_RESPONSE or SSH_AGENT_FAILURE,
* depending on whether we have that key or not.
*/
try {
var keyBlob = messageParser.ReadBlob();
var reqData = messageParser.ReadBlob();
var flags = new SignRequestFlags();
try {
// usually, there are no flags, so parser will throw
flags = (SignRequestFlags)messageParser.ReadInt();
} catch { }
var matchingKey =
mKeyList.Where(key => key.Version == SshVersion.SSH2 &&
key.GetPublicKeyBlob().SequenceEqual(keyBlob)).First();
var confirmConstraints = matchingKey.Constraints
.Where(constraint => constraint.Type ==
KeyConstraintType.SSH_AGENT_CONSTRAIN_CONFIRM);
if (confirmConstraints.Count() > 0) {
if (!ConfirmUserPermissionCallback.Invoke(matchingKey, process)) {
goto default;
}
}
/* create signature */
var signKey = matchingKey;
var signer = signKey.GetSigner();
var algName = signKey.Algorithm.GetIdentifierString();
signer.Init(true, signKey.GetPrivateKeyParameters());
signer.BlockUpdate(reqData, 0, reqData.Length);
byte[] signature = signer.GenerateSignature();
signature = signKey.FormatSignature(signature);
BlobBuilder signatureBuilder = new BlobBuilder();
if (!flags.HasFlag(SignRequestFlags.SSH_AGENT_OLD_SIGNATURE)) {
signatureBuilder.AddStringBlob(algName);
}
signatureBuilder.AddBlob(signature);
responseBuilder.AddBlob(signatureBuilder.GetBlob());
responseBuilder.InsertHeader(Message.SSH2_AGENT_SIGN_RESPONSE);
try {
KeyUsed(this, new KeyUsedEventArgs(signKey, process));
} catch { }
break; // succeeded
} catch (InvalidOperationException) {
// this is expected if there is not a matching key
} catch (Exception ex) {
Debug.Fail(ex.ToString());
}
goto default; // failure
case Message.SSH1_AGENTC_ADD_RSA_IDENTITY:
case Message.SSH1_AGENTC_ADD_RSA_ID_CONSTRAINED:
/*
* Add to the list and return SSH_AGENT_SUCCESS, or
* SSH_AGENT_FAILURE if the key was malformed.
*/
if (IsLocked) {
goto default;
}
bool ssh1constrained = (header.Message == Message.SSH1_AGENTC_ADD_RSA_ID_CONSTRAINED);
try {
var publicKeyParams = messageParser.ReadSsh1PublicKeyData(false);
var keyPair = messageParser.ReadSsh1KeyData(publicKeyParams);
SshKey key = new SshKey(SshVersion.SSH1, keyPair);
key.Comment = messageParser.ReadString();
key.Source = "External client";
if (ssh1constrained) {
while (messageStream.Position < header.BlobLength + 4) {
KeyConstraint constraint = new KeyConstraint();
constraint.Type = (KeyConstraintType)messageParser.ReadByte();
if (constraint.Type ==
KeyConstraintType.SSH_AGENT_CONSTRAIN_LIFETIME) {
constraint.Data = messageParser.ReadInt();
}
key.AddConstraint(constraint);
}
}
AddKey(key);
responseBuilder.InsertHeader(Message.SSH_AGENT_SUCCESS);
break;
} catch (CallbackNullException) {
// this is expected
} catch (Exception ex) {
Debug.Fail(ex.ToString());
}
goto default; // failed
case Message.SSH2_AGENTC_ADD_IDENTITY:
case Message.SSH2_AGENTC_ADD_ID_CONSTRAINED:
/*
* Add to the list and return SSH_AGENT_SUCCESS, or
* SSH_AGENT_FAILURE if the key was malformed.
*/
if (IsLocked) {
goto default;
}
bool constrained = (header.Message ==
Message.SSH2_AGENTC_ADD_ID_CONSTRAINED);
try {
var publicKeyParams = messageParser.ReadSsh2PublicKeyData();
var keyPair = messageParser.ReadSsh2KeyData(publicKeyParams);
SshKey key = new SshKey(SshVersion.SSH2, keyPair);
key.Comment = messageParser.ReadString();
key.Source = "External client";
if (constrained) {
while (messageStream.Position < header.BlobLength + 4) {
KeyConstraint constraint = new KeyConstraint();
constraint.Type =
(KeyConstraintType)messageParser.ReadByte();
if (constraint.Type ==
KeyConstraintType.SSH_AGENT_CONSTRAIN_LIFETIME) {
constraint.Data = messageParser.ReadInt();
}
key.AddConstraint(constraint);
}
}
AddKey(key);
responseBuilder.InsertHeader(Message.SSH_AGENT_SUCCESS);
break; // success!
} catch (CallbackNullException) {
// this is expected
} catch (Exception ex) {
Debug.Fail(ex.ToString());
}
goto default; // failed
case Message.SSH1_AGENTC_REMOVE_RSA_IDENTITY:
case Message.SSH2_AGENTC_REMOVE_IDENTITY:
/*
* Remove from the list and return SSH_AGENT_SUCCESS, or
* perhaps SSH_AGENT_FAILURE if it wasn't in the list to
* start with.
*/
if (IsLocked) {
goto default;
}
SshVersion removeVersion;
byte[] rKeyBlob;
if (header.Message == Message.SSH1_AGENTC_REMOVE_RSA_IDENTITY) {
removeVersion = SshVersion.SSH1;
rKeyBlob = messageParser.ReadBytes(header.BlobLength - 1);
} else if (header.Message == Message.SSH2_AGENTC_REMOVE_IDENTITY) {
removeVersion = SshVersion.SSH2;
rKeyBlob = messageParser.ReadBlob();
} else {
Debug.Fail("Should not get here.");
goto default;
}
try {
var matchingKey = mKeyList.Get(removeVersion, rKeyBlob);
var startKeyListLength = mKeyList.Count;
RemoveKey(matchingKey);
// only succeed if key was removed
if (mKeyList.Count == startKeyListLength - 1) {
responseBuilder.InsertHeader(Message.SSH_AGENT_SUCCESS);
break; //success!
}
} catch (Exception ex) {
Debug.Fail(ex.ToString());
}
goto default; // failed
case Message.SSH1_AGENTC_REMOVE_ALL_RSA_IDENTITIES:
case Message.SSH2_AGENTC_REMOVE_ALL_IDENTITIES:
/*
* Remove all SSH-1 or SSH-2 keys.
*/
if (IsLocked) {
goto default;
}
SshVersion removeAllVersion;
if (header.Message == Message.SSH1_AGENTC_REMOVE_ALL_RSA_IDENTITIES) {
removeAllVersion = SshVersion.SSH1;
} else if (header.Message == Message.SSH2_AGENTC_REMOVE_ALL_IDENTITIES) {
removeAllVersion = SshVersion.SSH2;
} else {
Debug.Fail("Should not get here.");
goto default;
}
try {
RemoveAllKeys(removeAllVersion);
responseBuilder.InsertHeader(Message.SSH_AGENT_SUCCESS);
break; //success!
} catch (Exception ex) {
Debug.Fail(ex.ToString());
}
goto default; // failed
case Message.SSH_AGENTC_LOCK:
try {
var passphrase = new PinnedArray<byte>(messageParser.ReadBlob());
try {
Lock(passphrase.Data);
} finally {
passphrase.Clear();
}
if (IsLocked) {
responseBuilder.InsertHeader(Message.SSH_AGENT_SUCCESS);
break;
}
} catch (AgentLockedException) {
// This is expected
} catch (Exception ex) {
Debug.Fail(ex.ToString());
}
goto default;
case Message.SSH_AGENTC_UNLOCK:
try {
var passphrase = new PinnedArray<byte>(messageParser.ReadBlob());
try {
Unlock(passphrase.Data);
} finally {
passphrase.Clear();
}
if (!IsLocked) {
responseBuilder.InsertHeader(Message.SSH_AGENT_SUCCESS);
break;
}
} catch (AgentLockedException) {
// This is expected
} catch (PassphraseException) {
// This is expected
} catch (Exception ex) {
Debug.Fail(ex.ToString());
}
goto default;
default:
responseBuilder.Clear();
responseBuilder.InsertHeader(Message.SSH_AGENT_FAILURE);
break;
}
/* write response to stream */
if (messageStream.CanSeek)
messageStream.Position = 0;
messageStream.Write(responseBuilder.GetBlob(), 0, responseBuilder.Length);
messageStream.Flush();
}