/// <summary> /// Reads the given xml and creates a hash code that uses every byte of the xml to contribute, then encrypts the resulting hash code using the key /// in the KeyedHashAlgorithm /// </summary> /// <param name="xml">the node of xml to sign</param> /// <param name="key">the "secret" key used to "sign" the hash</param> /// <returns>a base64-encoded string which is the encrypted signature of the xml</returns> string IValidator.Sign(XmlNode xml, string key) { byte[] outSignature = null; byte[] xmlNodeByte = null; // convert xmlnode contents into byte array xmlNodeByte = Encoding.Unicode.GetBytes(xml.InnerXml); try { // create an instance of keyed hash algo--note that the static Create is its own Factory method (sweet) KeyedHashAlgorithm kha = KeyedHashAlgorithm.Create(); // feed the hash algo the key kha.Key = Convert.FromBase64String(key); // key-hash xml outSignature = kha.ComputeHash(xmlNodeByte); } catch (Exception e) { ApplicationUpdateManager.TraceWrite(e, "[KeyValidator.Sign]", "RES_EXCEPTION_SigningXml"); throw e; } // no finally, no hard resources used // return key-hash return(Convert.ToBase64String(outSignature)); }
/// <summary> /// Initialize the Validator with the Public Key. This is cached after Init. /// Also set up the RSA-CSP and the deformatter. /// </summary> /// <param name="config">xml node from which we expect to extract single node "key" containing RSA public key</param> public void Init(XmlNode config) { try { // get the xml node that holds the key _keyNode = config.SelectSingleNode("key"); // initialize crypto objects for use by Validate overloads later: _rsaCSP = new RSACryptoServiceProvider(); // load key into CSP _rsaCSP.FromXmlString(_keyNode.OuterXml); // create de-formatter _rsaDeformatter = new RSAPKCS1SignatureDeformatter(_rsaCSP); // set hash algo to SHA1 _rsaDeformatter.SetHashAlgorithm(HASH_ALGO_SHA1); } catch (Exception e) { string error = ApplicationUpdateManager.TraceWrite(e, "[RSAValidator.Init]", "RES_CouldNotFindCryptographicKey"); throw new ArgumentException(error, "config", e); } }
/// <summary> /// Checks the download source location; BITS can only accept HTTP/HTTPS, so if /// we are given a UNC path as source, we need to stop right away--misconfiguration /// </summary> /// <param name="sourceFile">the path to the update file's source</param> private void ThrowIfSourceIsUNC(string sourceFile) { if (FileUtility.IsUNCPath(sourceFile)) { string error = ApplicationUpdateManager.TraceWrite("[BITSDownloader.BeginDownload]", "RES_EXCEPTION_UNCSourcePathToBITS", sourceFile); ArgumentException ae = new ArgumentException(error, "sourceFile"); throw ae; } }
/// <summary> /// Centralizes all chores related to stopping and cancelling a copy job, and getting back /// from BITS the errors incurred during the job. /// </summary> /// <param name="copyJob">reference to the copy job object (not job id)</param> /// <param name="errMessage">a cumulative error message passed by reference so /// that additions can be made</param> private void HandleDownloadErrorCancelJob( IBackgroundCopyJob copyJob, ref string errMessage) { string singleError = ""; string jobDesc = ""; string jobName = ""; Guid jobID = Guid.Empty; IBackgroundCopyError copyError = null; try { // check if job is null; don't try to clean up null references! if (null != copyJob) { // get information about this job for reporting the error copyJob.GetDescription(out jobDesc); copyJob.GetDisplayName(out jobName); copyJob.GetId(out jobID); // find out what the error was copyJob.GetError(out copyError); // use the culture id specified in RESX to tell COM which culture to return err message in: copyError.GetErrorDescription((uint)CULTURE_ID_FOR_COM, out singleError); // add error to our "stack" of errors: errMessage += singleError + Environment.NewLine; // notify BITS that we consider this job a loss, forget about it: copyJob.Cancel(); // remove job from collection RemoveCopyJobEntry(jobID); // log error, but don't throw here; let dnldmgr take care of error // NOTE that errMessage is used cumulatively for full track of problem errMessage = ApplicationUpdateManager.TraceWrite("[BITSDownloader]", "RES_EXCEPTION_BITSBackgroundCopyError", jobID, jobName, jobDesc, errMessage); ExceptionManager.Publish(new Exception(errMessage)); } } finally { if (null != copyError) { Marshal.ReleaseComObject(copyError); copyError = null; } } }
/// <summary> /// Initialize the provider /// </summary> void IValidator.Init(XmlNode config) { // get the xml node that holds the key (might be empty) XmlNode keyNode = config.SelectSingleNode("key"); // look for key in xml _key = ExtractKeyFromNode(keyNode); // we've tried to find key in "key" nodes, we should have a good key now; check to be sure if (null == _key) { string error = ApplicationUpdateManager.TraceWrite("[KeyValidator.Init]", "RES_CouldNotFindCryptographicKey"); CryptographicException ce = new CryptographicException(error); ExceptionManager.Publish(ce); throw ce; } }
/// <summary> /// Reads the given file and creates a hash code that uses every byte of the file to contribute, then encrypts the resulting hash code using the key /// in the KeyedHashAlgorithm /// </summary> /// <param name="filePath">full path to the file to sign</param> /// <param name="key">the "secret" key used to "sign" the hash</param> /// <returns>a base64-encoded string which is the encrypted signature of the file</returns> string IValidator.Sign(string filePath, string key) { byte[] outSignature = null; FileStream fs = null; try { // using the key (member variable) use hashed-key algorithm to generate // a signature for the file // attempt to open the file with shared read access fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); // create an instance of keyed hash algo--note that the static Create is its own Factory method (sweet) KeyedHashAlgorithm kha = KeyedHashAlgorithm.Create(); // feed the hash algo the key kha.Key = Convert.FromBase64String(key); // now hash over entire file, munging with key to give uniqueness that depends on both key and file // for a strong signature. // NOTE: now, the "secret" is the key. Without the "secret", you CANNOT spoof the identity of the file. // the contract between server and client is that the files are what the server promised; the enforcement of that // contract is the cryptographically strong hash, which depends on both key and file identity. // As long as both parties accept the validity of the "secret", they can trust the identity of the file if its signature matches. outSignature = kha.ComputeHash(fs); } catch (Exception e) { ApplicationUpdateManager.TraceWrite(e, "[KeyValidator.Sign]", "RES_EXCEPTION_SigningFile", filePath); throw e; } finally { if (null != fs) { fs.Close(); } } // return the signature return(Convert.ToBase64String(outSignature)); }
/// <summary> /// Helper function to get the base64-encoded key from the "key" node /// </summary> /// <param name="keyNode">contains the base64 encoded key hash</param> /// <returns>byte array which is the key decoded from base64</returns> private byte[] ExtractKeyFromNode(XmlNode keyNode) { byte[] key = null; // skip out right away if passed a null node if (null == keyNode) { return(null); } try { // we're passed the key node, so just take the innerText and convert to a byte[] from base64 key = Convert.FromBase64String(keyNode.InnerText); } catch (Exception e) { ApplicationUpdateManager.TraceWrite(e, "[KeyValidator.ExtractKeyFromNode]", "RES_CouldNotFindCryptographicKey"); ExceptionManager.Publish(e); throw e; } return(key); }
/// <summary> /// Synchronous downloading method using BITS /// </summary> /// <param name="sourceFile">Source file to download</param> /// <param name="destFile">Target file on the local system</param> /// <param name="maxTimeWait">Maximum time to wait for a download</param> void IDownloader.Download(string sourceFile, string destFile, TimeSpan maxTimeWait) { IBackgroundCopyManager backGroundCopyManager = null; IBackgroundCopyJob backGroundCopyJob = null; Guid jobID = Guid.Empty; bool isCompleted = false; bool isSuccessful = false; string cumulativeErrMessage = ""; BG_JOB_STATE state; // to defend against config errors, check to see if the path given is UNC; // if so, throw immediately there's a misconfiguration. Paths to BITS must be HTTP/HTTPS ThrowIfSourceIsUNC(sourceFile); try { // use utility function to create manager, job, get back jobid etc.; uses 'out' semantics CreateCopyJob( out backGroundCopyManager, out backGroundCopyJob, ref jobID, "RES_BITSJobName", "RES_BITSDescription"); // Add the file to the Job List backGroundCopyJob.AddFile(sourceFile, destFile); // Start the Back Ground Copy Job. backGroundCopyJob.Resume(); // set endtime to current tickcount + allowable # milliseconds to wait for job int endTime = Environment.TickCount + (int)maxTimeWait.TotalMilliseconds; #region __While Loop Waits On Single Download__ while (!isCompleted) { backGroundCopyJob.GetState(out state); switch (state) { case BG_JOB_STATE.BG_JOB_STATE_ERROR: { // use utility to: // a) get error info // b) report it // c) cancel and remove copy job HandleDownloadErrorCancelJob(backGroundCopyJob, ref cumulativeErrMessage); // complete loop, but DON'T say it's successful isCompleted = true; break; } case BG_JOB_STATE.BG_JOB_STATE_TRANSIENT_ERROR: { // NOTE: during debugging + test, transient errors resulted in complete job failure about 90% // of the time. Therefore we treat transients just like full errors, and CANCEL the job // use utility to manage error etc. HandleDownloadErrorCancelJob(backGroundCopyJob, ref cumulativeErrMessage); // stop the loop, set completed to true but not successful isCompleted = true; break; } case BG_JOB_STATE.BG_JOB_STATE_TRANSFERRED: { // notify BITS we're happy, remove from queue and transfer ownership to us: backGroundCopyJob.Complete(); // remove job from our collection, we won't need to Cancel() in our Dispose() RemoveCopyJobEntry(jobID); isCompleted = true; isSuccessful = true; break; } default: break; } if (endTime < Environment.TickCount) { HandleDownloadErrorCancelJob(backGroundCopyJob, ref cumulativeErrMessage); break; } // Avoid 100% CPU utilisation with too tight a loop, let download happen. Thread.Sleep(TIME_WAIT_SYNCH_DOWNLOAD); } #endregion if (!isSuccessful) { // create message + error, package it, publish string error = ApplicationUpdateManager.TraceWrite( "[BITSDownloader.Download]", "RES_MESSAGE_ManifestFileNotDownloaded", sourceFile, cumulativeErrMessage); Exception ex = new Exception(error + Environment.NewLine + cumulativeErrMessage); throw ex; } } catch (ThreadInterruptedException tie) { // if interrupted, clean up job HandleDownloadErrorCancelJob(backGroundCopyJob, ref cumulativeErrMessage); ApplicationUpdateManager.TraceWrite(tie, "[BITSDownloader.Download]", "RES_TIEInBITS", sourceFile); throw tie; } catch (Exception e) { // if exception, clean up job HandleDownloadErrorCancelJob(backGroundCopyJob, ref cumulativeErrMessage); // then log error string error = ApplicationUpdateManager.TraceWrite( e, "[BITSDownloader.Download]", "RES_MESSAGE_ManifestFileNotDownloaded", sourceFile, cumulativeErrMessage); Exception ex = new Exception(error, e); ExceptionManager.Publish(ex); // throw; allow consuming class to figure out what to do throw ex; } finally { if (null != backGroundCopyJob) { Marshal.ReleaseComObject(backGroundCopyJob); } if (null != backGroundCopyManager) { Marshal.ReleaseComObject(backGroundCopyManager); } } }
/// <summary> /// Internal copy-job factory method. Used to coordinate all aspects of a job set-up, /// which includes creating a copy manager, creating a job within it, setting download /// parameters, and adding the job to our tracking collection for cleanup later /// </summary> /// <param name="copyManager">null reference to copy manager</param> /// <param name="copyJob">null reference to copy job</param> /// <param name="jobID">null reference to job id guid</param> /// <param name="jobNameKey">the key used to look up the job name in the resource file</param> /// <param name="jobDescriptionKey">the key used to look up the job description in the resource file</param> private void CreateCopyJob( out IBackgroundCopyManager copyManager, out IBackgroundCopyJob copyJob, ref Guid jobID, string jobNameKey, string jobDescriptionKey) { string jobName = Resource.ResourceManager[jobNameKey]; string jobDesc = Resource.ResourceManager[jobDescriptionKey]; // wrap in try-finally so we can clean COM objects if unexpected error try { // create the manager copyManager = (IBackgroundCopyManager) new BackgroundCopyManager(); // create the job, set its description, use "out" semantics to get jobid and the actual job object copyManager.CreateJob( jobName, BG_JOB_TYPE.BG_JOB_TYPE_DOWNLOAD, out jobID, out copyJob); // set useful description copyJob.SetDescription(jobDesc); // *** // SET UP BITS JOB SETTINGS--TIMEOUTS/RETRY ETC // SEE THE FOLLOWING REFERENCES: // ** http://msdn.microsoft.com/library/default.asp?url=/library/en-us/bits/bits/ibackgroundcopyjob_setminimumretrydelay.asp?frame=true // ** http://msdn.microsoft.com/library/default.asp?url=/library/en-us/bits/bits/ibackgroundcopyjob_setnoprogresstimeout.asp?frame=true // ** http://msdn.microsoft.com/library/default.asp?url=/library/en-us/bits/bits/bg_job_priority.asp // *** // in constant set to 0; this makes BITS retry as soon as possible after an error copyJob.SetMinimumRetryDelay((uint)BITS_SET_MINIMUM_RETRY_DELAY); // in constant set to 5 seconds; BITS will set job to Error status if exceeded copyJob.SetNoProgressTimeout((uint)BITS_SET_NO_PROGRESS_TIMEOUT); // make this job the highest (but still background) priority-- copyJob.SetPriority(BG_JOB_PRIORITY.BG_JOB_PRIORITY_HIGH); // *** // lock our internal collection of jobs, and add this job--we use this collection in Dispose() // to tell BITS to Cancel() jobs--and remove them from the queue // if we did not do this, BITS would continue for (by default) two weeks to download what we asked! lock (_jobs.SyncRoot) { _jobs.Add(jobID, jobName); } } catch (Exception e) { // bad to catch all exceptions, but OK because we adorn it with necessary additional info then pass it up as innerException string error = ApplicationUpdateManager.TraceWrite(e, "[BITSDownloader.CreateCopyJob]", "RES_EXCEPTION_BITSOtherError", jobID, jobName, jobDesc); // publish Exception newE = new Exception(error, e); ExceptionManager.Publish(newE); // rethrow; throw newE; } }
/// <summary> /// returns a job status enum for a particular job identified by its GUID /// </summary> /// <param name="jobId">a guid for the job requested</param> /// <returns>a JobStatus describing the state of the job</returns> JobStatus IDownloader.GetJobStatus(Guid jobId) { IBackgroundCopyManager backGroundCopyManager = null; IBackgroundCopyJob backGroundCopyJob = null; BG_JOB_STATE state; string errMessage = ""; string jobName = ""; string jobDesc = ""; string error = ""; try { backGroundCopyManager = (IBackgroundCopyManager) new BackgroundCopyManager(); backGroundCopyManager.GetJob(ref jobId, out backGroundCopyJob); // get job name backGroundCopyJob.GetDisplayName(out jobName); // get job desc backGroundCopyJob.GetDescription(out jobDesc); // get job state enum value backGroundCopyJob.GetState(out state); switch (state) { case BG_JOB_STATE.BG_JOB_STATE_ERROR: { // use utility method to handle error: HandleDownloadErrorCancelJob(backGroundCopyJob, ref errMessage); // return status as error return(JobStatus.Error); } case BG_JOB_STATE.BG_JOB_STATE_TRANSIENT_ERROR: { // NOTE: if transient, just treat as full error. During testing about 90% of transients // resulted in full failure. Cleanup. // use utility method to handle error: HandleDownloadErrorCancelJob(backGroundCopyJob, ref errMessage); // return status as error return(JobStatus.Error); } case BG_JOB_STATE.BG_JOB_STATE_TRANSFERRED: { // tell BITS to transfer to us and stop thinking about the job backGroundCopyJob.Complete(); // remove job from collection to be Dispose()ed RemoveCopyJobEntry(jobId); return(JobStatus.Ready); } case BG_JOB_STATE.BG_JOB_STATE_CANCELLED: { // use utility method to handle error: HandleDownloadErrorCancelJob(backGroundCopyJob, ref errMessage); // return status as cancelled return(JobStatus.Cancelled); } default: return(JobStatus.Downloading); } } catch (ThreadInterruptedException tie) { // if interrupted, clean up job HandleDownloadErrorCancelJob(backGroundCopyJob, ref errMessage); ApplicationUpdateManager.TraceWrite(tie, "[BITSDownloader.Download]", "RES_TIEInBITS", "N/A"); throw tie; } catch (Exception e) { // use utility method to handle error: HandleDownloadErrorCancelJob(backGroundCopyJob, ref errMessage); // bad to catch all exceptions, but OK because we adorn it with necessary additional info then pass it up as innerException error = ApplicationUpdateManager.TraceWrite(e, "[BITSDownloader.GetJobStatus]", "RES_EXCEPTION_BITSOtherError", jobId, jobName, jobDesc); // publish Exception newE = new Exception(error, e); ExceptionManager.Publish(newE); // rethrow; throw newE; } finally { if (backGroundCopyManager != null) { Marshal.ReleaseComObject(backGroundCopyManager); } if (backGroundCopyJob != null) { Marshal.ReleaseComObject(backGroundCopyJob); } } }