public static long EncryptFile(System.IO.FileInfo SourceFile, FileStream DestinationFile, string[] Recipients, Keys SenderKeys)
        {
            // crypto variables
            // THESE SHOULD BE RANDOM!
            byte[] fileNonce = Utilities.GenerateRandomBytes(16);
            byte[] fileKey   = Utilities.GenerateRandomBytes(32);
            Keys   ephemeral = new Keys(true);

            // these are dependant on recipients
            byte[] sharedKey = null;
            // validate parameters

            //process chunks
            Blake2sCSharp.Hasher b2s  = Blake2sCSharp.Blake2S.Create();
            UTF8Encoding         utf8 = new UTF8Encoding();
            // use cache file instead of a memory stream to conserve used memory MemoryStream ms = new MemoryStream(); // processed chunks go here
            string     tempFile   = null;
            FileStream cacheFs    = GetTempFileStream(out tempFile);
            FileStream fs         = new FileStream(SourceFile.FullName, FileMode.Open, FileAccess.Read);
            long       fileCursor = 0;

            byte[] chunk      = null;
            UInt64 chunkCount = 0;

            byte[] chunkNonce = new byte[24];                    // always a constant length
            // this part of the nonce doesn't change
            Array.Copy(fileNonce, chunkNonce, fileNonce.Length); // copy it once and be done with it
            do
            {
                if (chunkCount == 0) // first chunk is always '\0'-padded filename
                {
                    chunk = new byte[256];
                    byte[] filename = utf8.GetBytes(SourceFile.Name);
                    Array.Copy(filename, chunk, filename.Length);
                    filename.Wipe(); // DON'T LEAK!!!
                }
                else
                {
                    if (fileCursor + MAX_CHUNK_SIZE >= SourceFile.Length)
                    {
                        // last chunk
                        chunkNonce[23] |= 0x80;
                        chunk           = new byte[SourceFile.Length - fileCursor];
                    }
                    else
                    {
                        chunk = new byte[MAX_CHUNK_SIZE];
                    }
                    if (fs.Read(chunk, 0, chunk.Length) != chunk.Length)
                    {
                        // read error!
                        fs.Close();
                        fs.Dispose();
                        TrashTempFileStream(cacheFs, tempFile);
                        throw new System.IO.IOException("Abrupt end of file / read error from source.");
                    }
                    fileCursor += chunk.Length;
                }
                byte[] outBuffer        = XSalsa20Poly1305.Encrypt(chunk, fileKey, chunkNonce);
                byte[] chunkLengthBytes = Utilities.UInt32ToBytes((uint)chunk.Length);
                cacheFs.Write(chunkLengthBytes, 0, 4);         // use cache file
                b2s.Update(chunkLengthBytes);                  // hash as we go
                cacheFs.Write(outBuffer, 0, outBuffer.Length); // use cache file
                b2s.Update(outBuffer);                         // hash as we go
                // since the first chunkNonce is just the fileNonce and a bunch of 0x00's,
                //  it's safe to do the chunk counter as a post-process update
                Utilities.UInt64ToBytes(++chunkCount, chunkNonce, 16);
            } while (fileCursor < SourceFile.Length);

            cacheFs.Flush(true);  // make sure everything is flushed to the disk cache
            cacheFs.Position = 0; // leave it open so that we can read it back into the destination
            // get the ciphertext hash for the header
            byte[] cipherTextHash = b2s.Finish();
            // done encrypting to the cache, now to build the header

            //build header (fileInfo needed first, but same for all recipients)...
            FileInfo fi = new FileInfo(
                fileKey.ToBase64String(),
                fileNonce.ToBase64String(),
                cipherTextHash.ToBase64String());

            byte[] fiBytes = utf8.GetBytes(fi.ToJSON()); // encrypt this to the recipients next...

            //build inner headers next (one for each recipient)
            Dictionary <string, string> innerHeaders = new Dictionary <string, string>(Recipients.Length);

            foreach (string recip in Recipients)
            {
                // each recipient is not identified in the outer header, only a random NONCE
                byte[] recipientNonce = Utilities.GenerateRandomBytes(24);
                sharedKey = // INNER SHARED KEY (Sender Secret + Recipient Public)
                            SenderKeys.GetShared(recip);
                InnerHeaderInfo ih = new InnerHeaderInfo(
                    SenderKeys.PublicID,
                    recip,
                    XSalsa20Poly1305.Encrypt(fiBytes, sharedKey, recipientNonce).ToBase64String()); // fileInfo JSON object encrypted, Base64
                sharedKey =                                                                         // OUTER SHARED KEY (Ephemeral Secret + Recipient Public)
                            ephemeral.GetShared(recip);
                string encryptedInnerHeader = ih.ToJSON();
                encryptedInnerHeader = XSalsa20Poly1305.Encrypt(utf8.GetBytes(encryptedInnerHeader), sharedKey, recipientNonce).ToBase64String();
                innerHeaders.Add(recipientNonce.ToBase64String(), encryptedInnerHeader);
            }
            // finally the outer header, ready for stuffing into the file
            HeaderInfo hi         = new HeaderInfo(1, ephemeral.PublicKey.ToBase64String(), innerHeaders);
            string     fileHeader = hi.ToJSON();

            // build the final file...
            DestinationFile.Write(utf8.GetBytes("miniLock"), 0, 8);                        // file identifier (aka "magic bytes")
            DestinationFile.Write(Utilities.UInt32ToBytes((uint)fileHeader.Length), 0, 4); // header length in 4 little endian bytes
            DestinationFile.Write(utf8.GetBytes(fileHeader), 0, fileHeader.Length);        // the full JSON header object
            // read back from the cache file into the destination file...
            byte[] buffer;
            for (int i = 0; i < cacheFs.Length; i += buffer.Length)
            {
                if (i + MAX_CHUNK_SIZE >= cacheFs.Length)
                {
                    buffer = new byte[cacheFs.Length - i];
                }
                else
                {
                    buffer = new byte[MAX_CHUNK_SIZE];
                }
                if (cacheFs.Read(buffer, 0, buffer.Length) != buffer.Length)
                {
                    throw new System.IO.IOException("Abrupt end of cache file");
                }
                DestinationFile.Write(buffer, 0, buffer.Length); // the ciphertext
            }
            // now flush and close, and grab length for reporting to caller
            DestinationFile.Flush();
            long tempOutputFileLength = DestinationFile.Length;

            DestinationFile.Close();
            DestinationFile.Dispose();
            // kill the cache and the directory created for it
            TrashTempFileStream(cacheFs, tempFile);

            return(tempOutputFileLength);
        }
        /// <summary>
        /// Decrypt a miniLock file using the specified Keys
        /// </summary>
        /// <param name="TheFile"></param>
        /// <param name="RecipientKeys"></param>
        /// <returns>null on any error, or a DecryptedFile object with the raw file contents, a plaintext hash,
        /// the SenderID, and the stored filename</returns>
        public static DecryptedFileDetails DecryptFile(FileStream SourceFile, string DestinationFileFullPath, bool OverWriteDestination, miniLockManaged.Keys RecipientKeys)
        {
            if (SourceFile == null)
            {
                throw new ArgumentNullException("SourceFile");
            }
            if (DestinationFileFullPath == null)
            {
                throw new ArgumentNullException("DestinationFile");
            }
            if (!SourceFile.CanRead)
            {
                throw new InvalidOperationException("Source File not readable!");
            }
            if (System.IO.File.Exists(DestinationFileFullPath) && !OverWriteDestination)
            {
                // be fault tolerant
                System.IO.FileInfo existing    = new System.IO.FileInfo(DestinationFileFullPath);
                string             newFilename = DestinationFileFullPath;
                int counter = 1;
                do
                {
                    newFilename  = DestinationFileFullPath.Replace(existing.Extension, "");
                    newFilename += '(' + counter++.ToString() + ')' + existing.Extension;
                } while (File.Exists(newFilename));
                DestinationFileFullPath = newFilename;
                // this is not fault tolerant
                //throw new InvalidOperationException("Destination File already exists!  Set OverWriteDestination true or choose a different filename.");
            }

            FullHeader fileStuff = new FullHeader();
            HeaderInfo h;

            byte[] buffer = null;

            // after this call, the source file pointer should be positioned to the end of the header
            int hLen = IngestHeader(ref SourceFile, out h);

            if (hLen < 0)
            {
                SourceFile.Close();
                SourceFile.Dispose();
                return(null);
            }
            hLen += 12;                                             // the 8 magic bytes and the 4 header length bytes and the length of the JSON header object
            long theCliff = SourceFile.Length - hLen;               // this is the ADJUSTED point where the file cursor falls off the cliff

            if (!TryDecryptHeader(h, RecipientKeys, out fileStuff)) // ciphertext hash is compared later
            {
                fileStuff.Clear();
                SourceFile.Close();
                SourceFile.Dispose();
                return(null);
            }

            Blake2sCSharp.Hasher b2sPlain  = Blake2sCSharp.Blake2S.Create(); // a nice-to-have for the user
            Blake2sCSharp.Hasher b2sCipher = Blake2sCSharp.Blake2S.Create(); // a check to make sure the ciphertext wasn't altered
            //note:  in theory, if the ciphertext doesn't decrypt at any point, there is likely something wrong with it up to and
            //  including truncation/extension
            //  BUT the hash is included in the header, and should be checked.

            DecryptedFileDetails results = new DecryptedFileDetails();

            results.ActualDecryptedFilePath = DestinationFileFullPath; // if the filename got changed, it happened before this point
            string tempFile = null;                                    // save the filename of the temp file so that the temp directory created with it is also killed

            System.IO.FileStream tempFS = GetTempFileStream(out tempFile);

            int    cursor      = 0;
            UInt64 chunkNumber = 0;

            byte[] chunkNonce = new byte[24];                                        // always a constant length
            Array.Copy(fileStuff.fileNonce, chunkNonce, fileStuff.fileNonce.Length); // copy it once and be done with it
            do
            {
                // how big is this chunk? (32bit number, little endien)
                buffer = new byte[4];
                if (SourceFile.Read(buffer, 0, buffer.Length) != buffer.Length)
                {
                    //read error
                    fileStuff.Clear();
                    SourceFile.Close();
                    SourceFile.Dispose();
                    TrashTempFileStream(tempFS, tempFile);
                    return(null);
                }
                b2sCipher.Update(buffer);  // have to include ALL the bytes, even the chunk-length bytes
                UInt32 chunkLength = Utilities.BytesToUInt32(buffer);
                if (chunkLength > MAX_CHUNK_SIZE)
                {
                    //something went wrong!
                    fileStuff.Clear();
                    SourceFile.Close();
                    SourceFile.Dispose();
                    TrashTempFileStream(tempFS, tempFile);
                    return(null);
                }
                cursor += 4; // move past the chunk length

                //the XSalsa20Poly1305 process, ALWAYS expands the plaintext by MacSizeInBytes
                // (authentication), so read the plaintext chunk length, add those bytes to the
                // value, then read that many bytes out of the ciphertext buffer
                byte[] chunk = new byte[chunkLength + XSalsa20Poly1305.MacSizeInBytes];
                //Array.Copy(buffer, cursor,
                //           chunk, 0,
                //           chunk.Length);
                if (SourceFile.Read(chunk, 0, chunk.Length) != chunk.Length)
                {
                    //read error
                    fileStuff.Clear();
                    SourceFile.Close();
                    SourceFile.Dispose();
                    TrashTempFileStream(tempFS, tempFile);
                    return(null);
                }
                b2sCipher.Update(chunk); // get hash of cipher text to compare to stored File Info Object
                cursor += chunk.Length;  // move the cursor past this chunk
                if (cursor >= theCliff)  // this is the last chunk
                {
                    // set most significant bit of nonce
                    chunkNonce[23] |= 0x80;
                }
                byte[] decryptBytes = XSalsa20Poly1305.TryDecrypt(chunk, fileStuff.fileKey, chunkNonce);
                if (decryptBytes == null)
                {
                    // nonce or key incorrect, or chunk has been altered (truncated?)
                    buffer = null;
                    fileStuff.Clear();
                    SourceFile.Close();
                    SourceFile.Dispose();
                    TrashTempFileStream(tempFS, tempFile);
                    return(null);
                }
                if (chunkNumber == 0) // first chunk is always filename '\0' padded
                {
                    results.StoredFilename = new UTF8Encoding().GetString(decryptBytes).Replace("\0", "").Trim();
                }
                else
                {
                    b2sPlain.Update(decryptBytes);                      // give the user a nice PlainText hash
                    tempFS.Write(decryptBytes, 0, decryptBytes.Length); // start building the output file
                }
                decryptBytes.Wipe();                                    // DON'T LEAK!!!
                // since the first chunkNonce is just the fileNonce and a bunch of 0x00's,
                //  it's safe to do the chunk number update as a post-process operation
                Utilities.UInt64ToBytes(++chunkNumber, chunkNonce, 16);
            } while (cursor < theCliff);
            SourceFile.Close();
            SourceFile.Dispose();
            byte[] ctActualHash = b2sCipher.Finish();
            if (!CryptoBytes.ConstantTimeEquals(ctActualHash, fileStuff.ciphertextHash))
            {
                // ciphertext was altered
                TrashTempFileStream(tempFS, tempFile);
                return(null);
            }
            results.SenderID = Keys.GetPublicIDFromKeyBytes(fileStuff.senderID);
            fileStuff.Clear(); // wipe the sensitive stuff!
            tempFS.Flush();
            tempFS.Close();
            tempFS.Dispose();
            //produce a handy hash for use by the end-user (not part of the spec)
            results.PlainTextBlake2sHash = b2sPlain.Finish().ToBase64String();

            System.IO.File.Move(tempFile, DestinationFileFullPath);
            // WARNING:  only use if the method that created the temp file also created a random subdir!
            Directory.Delete(new System.IO.FileInfo(tempFile).DirectoryName, true); // this is done since we didn't use TrashTempfileStream

            return(results);
        }