/// <summary> /// Encrypts a file asynchron with libsodium and protobuf-net. /// </summary> /// <param name="senderPrivateKey">A 32 byte private key.</param> /// <param name="senderPublicKey">A 32 byte public key.</param> /// <param name="recipientPublicKey">A 32 byte public key.</param> /// <param name="inputFile">The input file.</param> /// <param name="encryptionProgress">StreamCryptorTaskAsyncProgress object.</param> /// <param name="outputFolder">There the encrypted file will be stored, if this is null the input directory is used.</param> /// <param name="fileExtension">Set a custom file extenstion: .whatever</param> /// <param name="maskFileName">Replaces the filename with some random name.</param> /// <param name="cancellationToken">Token to request task cancellation.</param> /// <returns>The name of the encrypted file.</returns> /// <exception cref="ArgumentOutOfRangeException"></exception> /// <exception cref="FileNotFoundException"></exception> /// <exception cref="DirectoryNotFoundException"></exception> /// <exception cref="OperationCanceledException"></exception> public static async Task<string> EncryptFileWithStreamAsync(byte[] senderPrivateKey, byte[] senderPublicKey, byte[] recipientPublicKey, string inputFile, IProgress<StreamCryptorTaskAsyncProgress> encryptionProgress = null, string outputFolder = null, string fileExtension = DEFAULT_FILE_EXTENSION, bool maskFileName = false, CancellationToken cancellationToken = default(CancellationToken)) { string outputFullPath = String.Empty; string outputFile = String.Empty; //validate the senderPrivateKey if (senderPrivateKey == null || senderPrivateKey.Length != ASYNC_KEY_LENGTH) { throw new ArgumentOutOfRangeException("senderPrivateKey", "invalid senderPrivateKey"); } //validate the senderPublicKey if (senderPublicKey == null || senderPublicKey.Length != ASYNC_KEY_LENGTH) { throw new ArgumentOutOfRangeException("senderPublicKey", "invalid senderPublicKey"); } //validate the recipientPublicKey if (recipientPublicKey == null || recipientPublicKey.Length != ASYNC_KEY_LENGTH) { throw new ArgumentOutOfRangeException("recipientPublicKey", "invalid recipientPublicKey"); } //validate the inputFile if (string.IsNullOrEmpty(inputFile)) { throw new ArgumentOutOfRangeException("inputFile", (inputFile == null) ? 0 : inputFile.Length, string.Format("inputFile must be greater {0} in length.", 0)); } if (!File.Exists(inputFile)) { throw new FileNotFoundException("inputFile", "inputFile could not be found."); } //retrieve file info FileInfo inputFileInfo = new FileInfo(inputFile); if (inputFileInfo.Name.Length > MAX_FILENAME_LENGTH) { throw new ArgumentOutOfRangeException("inputFile", string.Format("inputFile name must be smaller {0} in length.", MAX_FILENAME_LENGTH)); } //validate the file extension if (!fileExtension[0].Equals('.')) { throw new ArgumentOutOfRangeException("fileExtension", "fileExtension must start with: ."); } //validate the outputFolder if (string.IsNullOrEmpty(outputFolder)) { //use the same directory as inputFile outputFolder = inputFileInfo.DirectoryName; } else { if (!Directory.Exists(outputFolder)) { throw new DirectoryNotFoundException("outputFolder could not be found."); } } //generate the name of the output file if (maskFileName) { //store the output file with a masked file name and the fileExtension outputFile = Utils.GetRandomFileName(MASKED_FILENAME_LENGTH, fileExtension); outputFullPath = Path.Combine(outputFolder, outputFile); } else { //store the output file, just with the fileExtension outputFile = inputFileInfo.Name + fileExtension; outputFullPath = Path.Combine(outputFolder, outputFile); } //go for the streams using (FileStream fileStreamEncrypted = File.OpenWrite(outputFullPath)) { using (FileStream fileStreamUnencrypted = File.OpenRead(inputFile)) { //initialize our file header for encryption EncryptedFileHeader encryptedFileHeader = new EncryptedFileHeader( CURRENT_VERSION, NONCE_LENGTH, CHUNK_BASE_NONCE_LENGTH, fileStreamUnencrypted.Length, senderPrivateKey, senderPublicKey, recipientPublicKey); //protect and set the file name to the header encryptedFileHeader.ProtectFileName(inputFileInfo.Name, MAX_FILENAME_LENGTH); //generate and set the checksum to validate our file header on decryption encryptedFileHeader.SetHeaderChecksum(HEADER_CHECKSUM_LENGTH); //write the file header to the stream Serializer.SerializeWithLengthPrefix(fileStreamEncrypted, encryptedFileHeader, PrefixStyle.Base128, 1); //we start at chunk number 0 int chunkNumber = CHUNK_COUNT_START; //used to calculate the footer checksum long overallChunkLength = 0; //used for progress reporting long overallBytesRead = 0; int bytesRead; do { //cancel the task if requested cancellationToken.ThrowIfCancellationRequested(); //start reading the unencrypted file in chunks of the given length: CHUNK_LENGTH byte[] unencryptedChunk = new byte[CHUNK_LENGTH]; bytesRead = await fileStreamUnencrypted.ReadAsync(unencryptedChunk, 0, CHUNK_LENGTH, cancellationToken).ConfigureAwait(false); //check if there is still some work if (bytesRead != 0) { //prepare the EncryptedFileChunk EncryptedFileChunk encryptedFileChunk = new EncryptedFileChunk(); byte[] readedBytes = new byte[bytesRead]; //cut unreaded bytes Array.Copy(unencryptedChunk, readedBytes, bytesRead); //check if the file is smaller or equal the CHUNK_LENGTH if (encryptedFileHeader.UnencryptedFileLength <= CHUNK_LENGTH) { //so we have the one and only chunk encryptedFileChunk = EncryptFileChunk(readedBytes, chunkNumber, encryptedFileHeader.BaseNonce, encryptedFileHeader.UnencryptedEphemeralKey, true); } else { //let`s check if this chunk is smaller than the given CHUNK_LENGTH if (bytesRead < CHUNK_LENGTH) { //it`s the last chunk in the stream encryptedFileChunk = EncryptFileChunk(readedBytes, chunkNumber, encryptedFileHeader.BaseNonce, encryptedFileHeader.UnencryptedEphemeralKey, true); } else { //it`s a full chunk encryptedFileChunk = EncryptFileChunk(readedBytes, chunkNumber, encryptedFileHeader.BaseNonce, encryptedFileHeader.UnencryptedEphemeralKey, false); } } overallChunkLength += encryptedFileChunk.Chunk.Length; //write encryptedFileChunk to the output stream Serializer.SerializeWithLengthPrefix(fileStreamEncrypted, encryptedFileChunk, PrefixStyle.Base128, 2); //increment for the next chunk chunkNumber++; overallBytesRead += bytesRead; //report status if (encryptionProgress != null) { var args = new StreamCryptorTaskAsyncProgress(); args.ProgressPercentage = (int)(encryptedFileHeader.UnencryptedFileLength <= 0 ? 0 : (100 * overallBytesRead) / encryptedFileHeader.UnencryptedFileLength); encryptionProgress.Report(args); } } else { //Prepare the EncryptedFileFooter for encryption. EncryptedFileFooter encryptedFileFooter = new EncryptedFileFooter(); //generate the FooterChecksum encryptedFileFooter.SetFooterChecksum(BitConverter.GetBytes(chunkNumber), BitConverter.GetBytes(overallChunkLength), encryptedFileHeader.UnencryptedEphemeralKey, FOOTER_CHECKSUM_LENGTH); //put the footer to the stream Serializer.SerializeWithLengthPrefix(fileStreamEncrypted, encryptedFileFooter, PrefixStyle.Base128, 3); } } while (bytesRead != 0); } } return outputFile; }
/// <summary> /// Decrypts a file asynchron into memory with libsodium and protobuf-net. /// </summary> /// <param name="recipientPrivateKey">A 32 byte private key.</param> /// <param name="inputFile">An encrypted file.</param> /// <param name="decryptionProgress">StreamCryptorTaskAsyncProgress object.</param> /// <param name="cancellationToken">Token to request task cancellation.</param> /// <returns>A DecryptedFile object.</returns> /// <remarks>This method can throw an OutOfMemoryException when there is not enough ram to hold the DecryptedFile!</remarks> /// <exception cref="ArgumentOutOfRangeException"></exception> /// <exception cref="FileNotFoundException"></exception> /// <exception cref="BadLastFileChunkException"></exception> /// <exception cref="BadFileChunkException"></exception> /// <exception cref="BadFileFooterException"></exception> /// <exception cref="BadFileHeaderException"></exception> /// <exception cref="IOException"></exception> /// <exception cref="OutOfMemoryException"></exception> /// <exception cref="OperationCanceledException"></exception> public static async Task<DecryptedFile> DecryptFileWithStreamAsync(byte[] recipientPrivateKey, string inputFile, IProgress<StreamCryptorTaskAsyncProgress> decryptionProgress = null, CancellationToken cancellationToken = default(CancellationToken)) { DecryptedFile decryptedFile = new DecryptedFile(); try { //validate the recipientPrivateKey if (recipientPrivateKey == null || recipientPrivateKey.Length != ASYNC_KEY_LENGTH) { throw new ArgumentOutOfRangeException("recipientPrivateKey", "invalid recipientPrivateKey"); } //validate the inputFile if (string.IsNullOrEmpty(inputFile)) { throw new ArgumentOutOfRangeException("inputFile", (inputFile == null) ? 0 : inputFile.Length, string.Format("inputFile must be greater {0} in length.", 0)); } if (!File.Exists(inputFile)) { throw new FileNotFoundException("inputFile", "inputFile could not be found."); } using (FileStream fileStreamEncrypted = File.OpenRead(inputFile)) { //first read the file header EncryptedFileHeader encryptedFileHeader = new EncryptedFileHeader(); encryptedFileHeader = Serializer.DeserializeWithLengthPrefix<EncryptedFileHeader>(fileStreamEncrypted, PrefixStyle.Base128, 1); //decrypt the ephemeral key with our public box byte[] ephemeralKey = PublicKeyBox.Open(encryptedFileHeader.Key, encryptedFileHeader.EphemeralNonce, recipientPrivateKey, encryptedFileHeader.SenderPublicKey); //validate our file header encryptedFileHeader.ValidateHeaderChecksum(ephemeralKey, HEADER_CHECKSUM_LENGTH); //check file header for compatibility if ((encryptedFileHeader.Version >= MIN_VERSION) && (encryptedFileHeader.BaseNonce.Length == CHUNK_BASE_NONCE_LENGTH)) { long overallChunkLength = 0; long overallBytesRead = 0; //restore the original file name byte[] encryptedPaddedFileName = SecretBox.Open(encryptedFileHeader.Filename, encryptedFileHeader.FilenameNonce, Utils.GetEphemeralEncryptionKey(ephemeralKey)); //remove the padding decryptedFile.FileName = Utils.PaddedByteArrayToString(encryptedPaddedFileName); //keep the position for the footer long fileStreamEncryptedPosition = 0; int chunkNumber = CHUNK_COUNT_START; //write the file to the tmpFullPath using (MemoryStream fileStreamUnencrypted = new MemoryStream()) { //start reading the chunks EncryptedFileChunk encryptedFileChunk = new EncryptedFileChunk(); while ((encryptedFileChunk = Serializer.DeserializeWithLengthPrefix<EncryptedFileChunk>(fileStreamEncrypted, PrefixStyle.Base128, 2)) != null) { //cancel the task if requested cancellationToken.ThrowIfCancellationRequested(); //indicates if ChunkIsLast was found, to prepend more than one last chnunks. bool isLastChunkFound = false; byte[] chunkNonce = new byte[NONCE_LENGTH]; //check if this is the last chunk if (encryptedFileChunk.ChunkIsLast) { if (!isLastChunkFound) { //last chunkNonce = GetChunkNonce(encryptedFileHeader.BaseNonce, chunkNumber, true); isLastChunkFound = true; } else { throw new BadLastFileChunkException("there are more than one last chunk, file could be damaged or manipulated!"); } } else { //there will propably come more chunkNonce = GetChunkNonce(encryptedFileHeader.BaseNonce, chunkNumber); } //check the current chunk checksum encryptedFileChunk.ValidateChunkChecksum(ephemeralKey, CHUNK_CHECKSUM_LENGTH); byte[] decrypted = SecretBox.Open(encryptedFileChunk.Chunk, chunkNonce, Utils.GetEphemeralEncryptionKey(ephemeralKey)); await fileStreamUnencrypted.WriteAsync(decrypted, 0, decrypted.Length, cancellationToken).ConfigureAwait(false); overallBytesRead += (long)decrypted.Length; chunkNumber++; overallChunkLength += encryptedFileChunk.ChunkLength; fileStreamEncryptedPosition = fileStreamEncrypted.Position; //report status if (decryptionProgress != null) { var args = new StreamCryptorTaskAsyncProgress(); args.ProgressPercentage = (int)(encryptedFileHeader.UnencryptedFileLength <= 0 ? 0 : (100 * overallBytesRead) / encryptedFileHeader.UnencryptedFileLength); decryptionProgress.Report(args); } } decryptedFile.FileData = fileStreamUnencrypted.ToArray(); decryptedFile.FileSize = decryptedFile.FileData.Length; } //set the last position fileStreamEncrypted.Position = fileStreamEncryptedPosition; //prepare the EncryptedFileFooter EncryptedFileFooter encryptedFileFooter = new EncryptedFileFooter(); //get the file footer and validate him encryptedFileFooter = Serializer.DeserializeWithLengthPrefix<EncryptedFileFooter>(fileStreamEncrypted, PrefixStyle.Base128, 3); if (encryptedFileFooter == null) { throw new BadFileFooterException("Missing file footer: file could be damaged or manipulated!"); } //validate the footer checksum encryptedFileFooter.ValidateFooterChecksum(BitConverter.GetBytes(chunkNumber), BitConverter.GetBytes(overallChunkLength), ephemeralKey, FOOTER_CHECKSUM_LENGTH); } else { throw new BadFileHeaderException("Incompatible file header: maybe different library version!"); } //check the produced output for the correct length if (encryptedFileHeader.UnencryptedFileLength != decryptedFile.FileSize) { //File is not valid (return null) decryptedFile = null; } } } catch (AggregateException ex) { //and throw the exception ExceptionDispatchInfo.Capture(ex).Throw(); } return decryptedFile; }
/// <summary> /// Decrypts a file asynchron with libsodium and protobuf-net. /// </summary> /// <param name="recipientPrivateKey">A 32 byte private key.</param> /// <param name="inputFile">An encrypted file.</param> /// <param name="outputFolder">There the decrypted file will be stored.</param> /// <param name="decryptionProgress">StreamCryptorTaskAsyncProgress object.</param> /// <param name="overWrite">Overwrite the output file if it exist.</param> /// <returns>The fullpath to the decrypted file.</returns> /// <remarks>This method needs a revision.</remarks> /// <exception cref="ArgumentOutOfRangeException"></exception> /// <exception cref="FileNotFoundException"></exception> /// <exception cref="DirectoryNotFoundException"></exception> /// <exception cref="BadLastFileChunkException"></exception> /// <exception cref="BadFileChunkException"></exception> /// <exception cref="BadFileFooterException"></exception> /// <exception cref="BadFileHeaderException"></exception> /// <exception cref="IOException"></exception> /// <exception cref="ArgumentException"></exception> public static async Task<string> DecryptFileWithStreamAsync(byte[] recipientPrivateKey, string inputFile, string outputFolder, IProgress<StreamCryptorTaskAsyncProgress> decryptionProgress = null, bool overWrite = false) { string outputFile = String.Empty; string outputFullPath = String.Empty; //used to check the file length of the unencrypted file, will be renamed to the outputFile (if the file is valid) string tmpFile = String.Empty; string tmpFullPath = String.Empty; try { //validate the recipientPrivateKey if (recipientPrivateKey == null || recipientPrivateKey.Length != ASYNC_KEY_LENGTH) { throw new ArgumentOutOfRangeException("recipientPrivateKey", "invalid recipientPrivateKey"); } //validate the inputFile if (string.IsNullOrEmpty(inputFile)) { throw new ArgumentOutOfRangeException("inputFile", (inputFile == null) ? 0 : inputFile.Length, string.Format("inputFile must be greater {0} in length.", 0)); } if (!File.Exists(inputFile)) { throw new FileNotFoundException("inputFile", "inputFile could not be found."); } //validate the outputFolder if (string.IsNullOrEmpty(outputFolder) || !Directory.Exists(outputFolder)) { throw new DirectoryNotFoundException("outputFolder must exist"); } if (outputFolder.IndexOfAny(Path.GetInvalidPathChars()) > -1) throw new ArgumentException("The given path to the output folder contains invalid characters!"); //get a tmp name tmpFile = Utils.GetRandomFileName(MASKED_FILENAME_LENGTH, TEMP_FILE_EXTENSION); tmpFullPath = Path.Combine(outputFolder, tmpFile); using (FileStream fileStreamEncrypted = File.OpenRead(inputFile)) { //first read the file header EncryptedFileHeader encryptedFileHeader = new EncryptedFileHeader(); encryptedFileHeader = Serializer.DeserializeWithLengthPrefix<EncryptedFileHeader>(fileStreamEncrypted, PrefixStyle.Base128, 1); //decrypt the ephemeral key with our public box byte[] ephemeralKey = PublicKeyBox.Open(encryptedFileHeader.Key, encryptedFileHeader.EphemeralNonce, recipientPrivateKey, encryptedFileHeader.SenderPublicKey); //validate our file header encryptedFileHeader.ValidateHeaderChecksum(ephemeralKey, HEADER_CHECKSUM_LENGTH); //check file header for compatibility if ((encryptedFileHeader.Version >= MIN_VERSION) && (encryptedFileHeader.BaseNonce.Length == CHUNK_BASE_NONCE_LENGTH)) { long overallChunkLength = 0; long overallBytesRead = 0; //restore the original file name byte[] decryptedPaddedFileName = SecretBox.Open(encryptedFileHeader.Filename, encryptedFileHeader.FilenameNonce, Utils.GetEphemeralEncryptionKey(ephemeralKey)); //remove the padding outputFile = Utils.PaddedByteArrayToString(decryptedPaddedFileName); //check the decrypted outputFile name for invalid characters to prevent directory traversal if (outputFile.IndexOfAny(Path.GetInvalidFileNameChars()) > -1) throw new ArgumentException("The given name of the decrypted output filename contains invalid characters!"); outputFullPath = Path.Combine(outputFolder, outputFile); //check for the correct result of Path.Combine if (!outputFullPath.StartsWith(outputFolder)) throw new ArgumentException("The given output path seems to be bad!"); //keep the position for the footer long fileStreamEncryptedPosition = 0; int chunkNumber = CHUNK_COUNT_START; //write the file to the tmpFullPath using (FileStream fileStreamUnencrypted = File.OpenWrite(tmpFullPath)) { //start reading the chunks EncryptedFileChunk encryptedFileChunk = new EncryptedFileChunk(); while ((encryptedFileChunk = Serializer.DeserializeWithLengthPrefix<EncryptedFileChunk>(fileStreamEncrypted, PrefixStyle.Base128, 2)) != null) { //indicates if ChunkIsLast was found, to prepend more than one last chnunks. bool isLastChunkFound = false; byte[] chunkNonce = new byte[NONCE_LENGTH]; //check if this is the last chunk if (encryptedFileChunk.ChunkIsLast) { if (!isLastChunkFound) { //last chunkNonce = GetChunkNonce(encryptedFileHeader.BaseNonce, chunkNumber, true); isLastChunkFound = true; } else { throw new BadLastFileChunkException("there are more than one last chunk, file could be damaged or manipulated!"); } } else { //there will propably come more chunkNonce = GetChunkNonce(encryptedFileHeader.BaseNonce, chunkNumber, false); } //check the current chunk checksum encryptedFileChunk.ValidateChunkChecksum(ephemeralKey, CHUNK_CHECKSUM_LENGTH); byte[] decrypted = SecretBox.Open(encryptedFileChunk.Chunk, chunkNonce, Utils.GetEphemeralEncryptionKey(ephemeralKey)); await fileStreamUnencrypted.WriteAsync(decrypted, 0, decrypted.Length).ConfigureAwait(false); overallBytesRead += (long)decrypted.Length; chunkNumber++; overallChunkLength += encryptedFileChunk.ChunkLength; fileStreamEncryptedPosition = fileStreamEncrypted.Position; //report status if (decryptionProgress != null) { var args = new StreamCryptorTaskAsyncProgress(); args.ProgressPercentage = (int)(encryptedFileHeader.UnencryptedFileLength <= 0 ? 0 : (100 * overallBytesRead) / encryptedFileHeader.UnencryptedFileLength); decryptionProgress.Report(args); } } } //set the last position fileStreamEncrypted.Position = fileStreamEncryptedPosition; //prepare the EncryptedFileFooter EncryptedFileFooter encryptedFileFooter = new EncryptedFileFooter(); //get the file footer and validate him encryptedFileFooter = Serializer.DeserializeWithLengthPrefix<EncryptedFileFooter>(fileStreamEncrypted, PrefixStyle.Base128, 3); if (encryptedFileFooter == null) { throw new BadFileFooterException("Missing file footer: file could be damaged or manipulated!"); } //validate the footer checksum encryptedFileFooter.ValidateFooterChecksum(BitConverter.GetBytes(chunkNumber), BitConverter.GetBytes(overallChunkLength), ephemeralKey, FOOTER_CHECKSUM_LENGTH); } else { throw new BadFileHeaderException("Incompatible file header: maybe different library version!"); } //check the produced output for the correct length if (encryptedFileHeader.UnencryptedFileLength == new FileInfo(tmpFullPath).Length) { //check if the new output file already exists if (File.Exists(outputFullPath)) { if (!overWrite) { //we don`t overwrite the file throw new IOException("Decrypted file aleary exits, won`t overwrite"); } else { //just delete the output file, so we can write a new one File.Delete(outputFullPath); } } File.Move(tmpFullPath, outputFullPath); } else { //File is not valid (return null) outputFile = null; File.Delete(tmpFullPath); } } } catch (AggregateException ex) { //delete the temp file File.Delete(tmpFullPath); //and throw the exception ExceptionDispatchInfo.Capture(ex).Throw(); } return outputFile; }