#region Copyright 2011-2014 by Roger Knapp, Licensed under the Apache License, Version 2.0 /* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #endregion using System; using System.IO; using System.ComponentModel; using System.Security.Cryptography; using System.Threading; using CSharpTest.Net.IO; using CSharpTest.Net.Threading; using CSharpTest.Net.Bases; using CSharpTest.Net.Interfaces; namespace CSharpTest.Net.Crypto { /// /// SecureTransfer is a static class that contains two user types, Sender and Receiver. Each provide /// one-half of a secure file transfer protocol. The security is based on pre-shared public keys used /// to sign all messages between the client and server. Additionally these keys are used durring the /// session negotiation to exchange a 256-bit session key. The session key is combined with a random /// salt for each message to produce an AES-256 cryptographic key. The file content is then tranfered /// with this session key. /// public static partial class SecureTransfer { /// /// Provides a file transfer handler for the client-side of file transfers. /// public class Client { private readonly RSAPrivateKey _privateKey; private readonly RSAPublicKey _publicKey; private readonly TransmitMessageAction _sendMessage; private readonly ManualResetEvent _abort; /// /// Provides the transmission of a stream of bytes to the server/receiver and returns the result stream /// public delegate Stream TransmitMessageAction(Guid transferId, string location, Stream request); /// /// Constructed to send one or more files to a remove server identified by serverKey. The transfer /// is a blocking call and returns on success or raises an exception. If Abort() is called durring /// the transfer, or if a ProgressChanged event handler raises the OperationCanceledException, the /// transfer is silently terminated. /// /// The private key for this client /// The public key of the server /// A delegate to transfer data to the server and obtain a response public Client(RSAPrivateKey privateKey, RSAPublicKey serverKey, TransmitMessageAction sendMessage) { _privateKey = Check.NotNull(privateKey); _publicKey = Check.NotNull(serverKey); _sendMessage = Check.NotNull(sendMessage); _abort = new ManualResetEvent(false); LimitThreads = 10; } /// The maximum number of concurrent calls to the server public int LimitThreads { get; set; } /// /// Raised after each block of data is transferred to the server. /// public event ProgressChangedEventHandler ProgressChanged; private void OnProgressChanged(string location, long current, long total) { ProgressChangedEventHandler handler = ProgressChanged; if (handler != null) { long percent = (current*100L)/total; try { handler(this, new ProgressChangedEventArgs((int)percent, location)); } catch (OperationCanceledException) { Abort(); } } } /// Aborts the transfer public void Abort() { _abort.Set(); } private Stream SendPayload(Message req, string location, Stream stream) { return _sendMessage(req.TransferId, location, stream); } /// /// Called to send a single file to the remove server identified by serverKey. The transfer /// is a blocking call and returns on success or raises an exception. If Abort() is called durring /// the transfer, or if a ProgressChanged event handler raises the OperationCanceledException, the /// transfer is silently terminated and the method will return false. /// /// A string of up to 1024 bytes in length /// The file path of the file to transfer /// True if the file was successfully received by the server public bool Upload(string location, string filePath) { using (Stream input = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) return Upload(location, input.Length, input); } /// /// Called to send a specific length of bytes to a server identified by serverKey. The transfer /// is a blocking call and returns on success or raises an exception. If Abort() is called durring /// the transfer, or if a ProgressChanged event handler raises the OperationCanceledException, the /// transfer is silently terminated and the method will return false. /// /// A string of up to 1024 bytes in length /// The length in bytes to send from the stream /// The stream to read the data from /// True if the file was successfully received by the server public bool Upload(string location, long length, Stream rawInput) { Guid transferId = Guid.NewGuid(); int maxMessageLength; // STEP 1: Send a NonceRequest, Create Salt sessionKey = BeginUpload(transferId, location, length, out maxMessageLength); // STEP 2: Send the data Hash fullHash; bool[] failed = new bool[1]; using (HashStream input = new HashStream(new SHA256Managed(), rawInput)) using (WorkQueue queue = new WorkQueue(LimitThreads)) { queue.OnError += (o, e) => { failed[0] = true; }; long pos = 0; while (pos < length && !failed[0] && !_abort.WaitOne(0, false)) { int len = (int)Math.Min(length - pos, maxMessageLength); byte[] buffer = new byte[len]; IOStream.Read(input, buffer, len); BytesToSend task = new BytesToSend(this, LimitThreads, transferId, sessionKey, location, pos, buffer); queue.Enqueue(task.Send); OnProgressChanged(location, pos, length); pos += len; } queue.Complete(true, failed[0] ? 5000 : 300000); fullHash = input.FinalizeHash();//hash of all data transferred } if (_abort.WaitOne(0, false)) return false; Check.Assert(failed[0] == false); // STEP 4: Complete the transfer CompleteUpload(transferId, sessionKey, location, fullHash); OnProgressChanged(location, length, length); return true; } /// /// Called to send a specific length of bytes to a server identified by serverKey. The transfer /// is a blocking call and returns on success or raises an exception. If Abort() is called durring /// the transfer, or if a ProgressChanged event handler raises the OperationCanceledException, the /// transfer is silently terminated and the method will return false. /// /// A string of up to 1024 bytes in length /// Any writable stream that can seek /// True if the file was successfully received by the server public bool Download(string location, Stream output) { Check.Assert(output.CanSeek && output.CanWrite, "The download stream must be able to seek and write."); return Download(location, new StreamCache(new InstanceFactory(output), 1)); } /// /// Called to send a specific length of bytes to a server identified by serverKey. The transfer /// is a blocking call and returns on success or raises an exception. If Abort() is called durring /// the transfer, or if a ProgressChanged event handler raises the OperationCanceledException, the /// transfer is silently terminated and the method will return false. /// /// A string of up to 1024 bytes in length /// The name of the file to write to /// True if the file was successfully received by the server public bool Download(string location, string filename) { FileStreamFactory file = new FileStreamFactory(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite); using (TempFile temp = TempFile.Attach(filename)) using (StreamCache cache = new StreamCache(file, LimitThreads)) { temp.Length = 0; if (Download(location, cache)) { temp.Detatch(); return true; } } return false; } private static Salt SessionSecret(Salt clientKeyBits, byte[] serverKeyBits) { Salt sessionSecret = Salt.FromBytes( Hash.SHA256( new CombinedStream( clientKeyBits.ToStream(), new MemoryStream(serverKeyBits, false) ) ).ToArray() ); return sessionSecret; } private byte[] GetNonce(Guid transferId, string location, out byte[] serverKeyBits) { byte[] nonce; // STEP 1: Send a NonceRequest using (Message req = new Message(TransferState.NonceRequest, transferId, _publicKey, NoSession)) { Stream response = SendPayload(req, location, req.ToStream(_privateKey)); using (Message rsp = new Message(response, _privateKey, NoSession)) { Check.Assert(rsp.State == TransferState.NonceResponse); nonce = rsp.ReadBytes(1024); serverKeyBits = rsp.ReadBytes(1024); rsp.VerifySignature(_publicKey); } } return nonce; } private bool Download(string location, StreamCache output) { int maxMessageLength; long fileLength, bytesReceived = 0; Guid transferId = Guid.NewGuid(); byte[] serverKeyBits; byte[] nonce = GetNonce(transferId, location, out serverKeyBits); Hash hnonce = Hash.SHA256(nonce); //STEP 2: Create and send session key Salt clientKeyBits = new Salt(Salt.Size.b256); Salt sessionKey = SessionSecret(clientKeyBits, serverKeyBits); using (Message req = new Message(TransferState.DownloadRequest, transferId, _publicKey, NoSession)) { req.Write(hnonce.ToArray()); req.Write(location); req.Write(clientKeyBits.ToArray()); Stream response = SendPayload(req, location, req.ToStream(_privateKey)); using (Message rsp = new Message(response, _privateKey, s=>sessionKey)) { Check.Assert(rsp.State == TransferState.DownloadResponse); maxMessageLength = Check.InRange(rsp.ReadInt32(), 0, int.MaxValue); fileLength = Check.InRange(rsp.ReadInt64(), 0, 0x00FFFFFFFFFFFFFFL); byte[] bytes = rsp.ReadBytes(100 * 1000 * 1024); rsp.VerifySignature(_publicKey); using(Stream io = output.Open(FileAccess.Write)) { io.SetLength(fileLength); if (bytes.Length > 0) { io.Seek(0, SeekOrigin.Begin); io.Write(bytes, 0, bytes.Length); bytesReceived += bytes.Length; } } } } //STEP 3...n: Continue downloading other chunks of the file if (bytesReceived < fileLength) { bool[] failed = new bool[1]; using (WorkQueue queue = new WorkQueue(LimitThreads)) { queue.OnError += (o, e) => { failed[0] = true; }; while (bytesReceived < fileLength && !failed[0] && !_abort.WaitOne(0, false)) { int len = (int) Math.Min(fileLength - bytesReceived, maxMessageLength); BytesToRead task = new BytesToRead( this, LimitThreads, transferId, sessionKey, location, output, bytesReceived, len); queue.Enqueue(task.Send); OnProgressChanged(location, bytesReceived, fileLength); bytesReceived += len; } queue.Complete(true, failed[0] ? 5000 : 7200000); } if (_abort.WaitOne(0, false)) return false; Check.Assert(failed[0] == false); // STEP 4: Complete the transfer using (Message req = new Message(TransferState.DownloadCompleteRequest, transferId, _publicKey, s => sessionKey)) { SendPayload(req, location, req.ToStream(_privateKey)).Dispose(); } } OnProgressChanged(location, fileLength, fileLength); return true; } private Salt BeginUpload(Guid transferId, string location, long length, out int maxMessageLength) { byte[] serverKeyBits; byte[] nonce = GetNonce(transferId, location, out serverKeyBits); Hash hnonce = Hash.SHA256(nonce); //STEP 2: Create and send session key Salt clientKeyBits = new Salt(Salt.Size.b256); Salt sessionSecret = SessionSecret(clientKeyBits, serverKeyBits); using (Message req = new Message(TransferState.UploadRequest, transferId, _publicKey, NoSession)) { req.Write(hnonce.ToArray()); req.Write(length); req.Write(location); req.Write(clientKeyBits.ToArray()); Stream response = SendPayload(req, location, req.ToStream(_privateKey)); using (Message rsp = new Message(response, _privateKey, s=>sessionSecret)) { Check.Assert(rsp.State == TransferState.UploadResponse); maxMessageLength = Check.InRange(rsp.ReadInt32(), 0, int.MaxValue); rsp.VerifySignature(_publicKey); } } return sessionSecret; } private void TransferBytes(Guid transferId, Salt sessionKey, string location, long offset, byte[] bytes) { // STEP 3...n: Send a block of bytes using (Message req = new Message(TransferState.SendBytesRequest, transferId, _publicKey, s => sessionKey)) { req.Write(offset); req.Write(bytes); Stream response = SendPayload(req, location, req.ToStream(_privateKey)); using (Message rsp = new Message(response, _privateKey, s => sessionKey)) { Check.Assert(rsp.State == TransferState.SendBytesResponse); Check.Assert(rsp.ReadInt64() == offset); rsp.VerifySignature(_publicKey); } } } private void ReadByteRange(Guid transferId, Salt sessionKey, string location, StreamCache streams, long offset, int count) { using (Message req = new Message(TransferState.DownloadBytesRequest, transferId, _publicKey, s=>sessionKey)) { req.Write(location); req.Write(offset); req.Write(count); Stream response = SendPayload(req, location, req.ToStream(_privateKey)); using (Message rsp = new Message(response, _privateKey, s=>sessionKey)) { Check.Assert(rsp.State == TransferState.DownloadBytesResponse); byte[] bytes = rsp.ReadBytes(100 * 1000 * 1024); Check.Assert(bytes.Length == count); rsp.VerifySignature(_publicKey); using(Stream io = streams.Open(FileAccess.Write)) { io.Seek(offset, SeekOrigin.Begin); io.Write(bytes, 0, count); } } } } private void CompleteUpload(Guid transferId, Salt sessionKey, string location, Hash fullHash) { // STEP 4: Finalize the transfer using (Message req = new Message(TransferState.UploadCompleteRequest, transferId, _publicKey, s => sessionKey)) { req.Write(location); req.Write(fullHash.ToArray()); Stream response = SendPayload(req, location, req.ToStream(_privateKey)); using (Message rsp = new Message(response, _privateKey, s => sessionKey)) { Check.Assert(rsp.State == TransferState.UploadCompleteResponse); rsp.VerifySignature(_publicKey); } } } #region BytesToSend private class BytesToSend : Disposable { private bool _aquired; private readonly Semaphore _throttle; private readonly Client _client; private readonly Guid _transferId; private readonly Salt _sessionKey; private readonly string _location; private readonly byte[] _bytes; private readonly long _offset; public BytesToSend(Client client, int threadLimit, Guid transferId, Salt sessionKey, string location, long offset, byte[] bytes) { _client = client; _transferId = transferId; _sessionKey = sessionKey; _location = location; _bytes = bytes; _offset = offset; _throttle = new Semaphore(threadLimit, threadLimit, GetType().FullName); _throttle.WaitOne(); _aquired = true; } protected override void Dispose(bool disposing) { if (_aquired) { _aquired = false; _throttle.Release(); if (disposing) _throttle.Close(); } } public void Send() { using(this) _client.TransferBytes(_transferId, _sessionKey, _location, _offset, _bytes); } } #endregion #region BytesToRead private class BytesToRead : Disposable { private bool _aquired; private readonly Semaphore _throttle; private readonly Client _client; private readonly Guid _transferId; private readonly Salt _sessionKey; private readonly string _location; private readonly StreamCache _streams; private readonly long _offset; private readonly int _count; public BytesToRead(Client client, int threadLimit, Guid transferId, Salt sessionKey, string location, StreamCache streams, long offset, int count) { _client = client; _transferId = transferId; _sessionKey = sessionKey; _location = location; _streams = streams; _offset = offset; _count = count; _throttle = new Semaphore(threadLimit, threadLimit, GetType().FullName); _throttle.WaitOne(); _aquired = true; } protected override void Dispose(bool disposing) { if (_aquired) { _aquired = false; _throttle.Release(); if (disposing) _throttle.Close(); } } public void Send() { using(this) _client.ReadByteRange(_transferId, _sessionKey, _location, _streams, _offset, _count); } } #endregion } } }