Search code examples
c#androidxamarin.formsaesfirebase-storage

Encrypt and Upload & Decrypt and Download Image to/from Firebase Storage in Xamarin Forms


I am attempting to encrypt an image with AES and upload to it to Firebase Storage and also decrypt and download it. I am using the following methods:

private async void BtnUpload_Clicked(object sender, EventArgs e)
        {
            var fileStream = FileEncrypt(file.Path);
            var user = await GetUser(localEmail);
            await firebaseHelper.UploadFile(fileStream.Result, Path.GetFileName(file.Path), user.UserID);
            var downloadurl = await firebaseHelper.GetFile(Path.GetFileName(file.Path), user.UserID);
            await firebaseHelper.UploadURL(Path.GetFileName(file.Path), downloadurl.ToString(), user.UserID);
            await DisplayAlert("Success", "Uploaded", "OK");
        }
private async Task<FileStream> FileEncrypt(string inputFile)
        {
            var user = await GetUser(localEmail);
            FileStream fsCrypt = new FileStream(inputFile + ".aes", FileMode.Create);
            //Set Rijndael symmetric encryption algorithm
            RijndaelManaged AES = new RijndaelManaged();
            AES.KeySize = 256;
            AES.BlockSize = 128;
            AES.Padding = PaddingMode.PKCS7;
            var key = new Rfc2898DeriveBytes(user.Key, user.Salt, 50000);
            AES.Key = key.GetBytes(AES.KeySize / 8);
            AES.IV = key.GetBytes(AES.BlockSize / 8);
            AES.Mode = CipherMode.CFB;
            // write salt to the begining of the output file
            fsCrypt.Write(user.Salt, 0, user.Salt.Length);
            CryptoStream cs = new CryptoStream(fsCrypt, AES.CreateEncryptor(), CryptoStreamMode.Write);
            FileStream fsIn = new FileStream(inputFile, FileMode.Open);

            //create a buffer (1mb) so only this amount will allocate in the memory and not the whole file
            //1048576 is 1MB in binary
            byte[] buffer = new byte[1048576];
            int read;
            try
            {
                while ((read = fsIn.Read(buffer, 0, buffer.Length)) > 0)
                {
                    //Application.DoEvents(); // -> for responsive GUI, using Task will be better!
                    cs.Write(buffer, 0, read);
                }
                //fsIn.Close();//causes error
            }
            catch (Exception ex)
            {
                DisplayAlert("Error", "Error: " + ex.Message, "Ok");
            }
            finally
            {
                cs.Close();
                fsCrypt.Close();
            }
            return fsIn;
        }

However, when I am attempting to download the encrypted file and save to users Android gallery, it doesn't recognise the file path and throws an error saying part of the file path was not found. I've checked to make sure the file path is correct. (If I upload the image without encrypting and then use the download url as the source for an Image preview box that I have, it successfully displays the image, hence I deduced the download url I am using is correct).

Can anyone help?

Decrypt and Download method below:

 private async void FileDecrypt(string inputFile, string outputFile)
        {
            var user = await GetUser(localEmail);
            FileStream fsCrypt = new FileStream(inputFile, FileMode.Open);
            fsCrypt.Read(user.Salt, 0, user.Salt.Length);

            RijndaelManaged AES = new RijndaelManaged();
            AES.KeySize = 256;
            AES.BlockSize = 128;
            var key = new Rfc2898DeriveBytes(user.Key, user.Salt, 50000);
            AES.Key = key.GetBytes(AES.KeySize / 8);
            AES.IV = key.GetBytes(AES.BlockSize / 8);
            AES.Padding = PaddingMode.PKCS7;
            AES.Mode = CipherMode.CFB;
            CryptoStream cs = new CryptoStream(fsCrypt, AES.CreateDecryptor(), CryptoStreamMode.Read);
            FileStream fsOut = new FileStream(outputFile, FileMode.Create);
            int read;
            byte[] buffer = new byte[1048576];
            try
            {
                while ((read = cs.Read(buffer, 0, buffer.Length)) > 0)
                {
                    //Application.DoEvents();
                    fsOut.Write(buffer, 0, read);
                }
            }
            catch (CryptographicException ex_CryptographicException)
            {
                DisplayAlert("Error", "CryptographicException error: " + ex_CryptographicException.Message, "Ok");
            }
            catch (Exception ex)
            {
                DisplayAlert("Error", "Error: " + ex.Message, "Ok");
            }
            try
            {
                cs.Close();
            }
            catch (Exception ex)
            {
                DisplayAlert("Error", "Error by closing CryptoStream: " + ex.Message, "Ok");
            }
            finally
            {
                fsOut.Close();
                fsCrypt.Close();
            }
        }

private async void BtnDownload_Clicked(object sender, EventArgs e)
        {
            //output path
            string galleryPath = Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryPictures).AbsolutePath;
            string outputPath = Path.Combine(galleryPath, localDownloadUrl.FileName);

            FileDecrypt(localDownloadUrl.Url, outputPath);
            await DisplayAlert("Success", "Image saved to gallery", "OK");
            await App.Current.MainPage.Navigation.PopModalAsync();
        }

Solution

  • So, I've managed to finally solve this with a lot of help from @Jason (refer to comments).

    In my question I posted, the FileEncrypt method is taking a file (whose path you pass into the method as a parameter), opening it and reading it using the file stream fsIn, encrypting it using the crypto stream cs and then writing the encrypted file out to some location using the file stream fsCrypt. To upload it to Firebase Storage, I passed in the path to some folder in my phone to the file stream that writes out the encrypted file. This results in the encrypted file being saved into that folder. I then get the file stream to this encrypted file that's now saved in my phone and pass it to the UploadFile method as a parameter which then uploads it to Firebase Storage. Methods below:

    private async void FileEncrypt(string inputFile)
            {
                //this is the path to where the encrypted file will be saved, I have folder named Vault there, I've added the .aes extension so I know it's encrypted.
                string galleryPath = Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryPictures).AbsolutePath;
                string outputPath = Path.Combine(galleryPath + "/Vault", Path.GetFileName(file.Path) + ".aes");
                var user = await GetUser(localEmail);//gets user object (this is my own method)
                FileStream fsCrypt = new FileStream(outputPath, FileMode.Create);//used to create the encrypted file
                //Set Rijndael symmetric encryption algorithm
                RijndaelManaged AES = new RijndaelManaged();
                AES.KeySize = 256;
                AES.BlockSize = 128;
                AES.Padding = PaddingMode.Zeros;
                var key = new Rfc2898DeriveBytes(user.Key, user.Salt, 50000);
                AES.Key = key.GetBytes(AES.KeySize / 8);
                AES.IV = key.GetBytes(AES.BlockSize / 8);
                AES.Mode = CipherMode.CFB;
                // write salt to the beginning of the output file
                fsCrypt.Write(user.Salt, 0, user.Salt.Length);
                CryptoStream cs = new CryptoStream(fsCrypt, AES.CreateEncryptor(), CryptoStreamMode.Write);//used to encrypt the contents of your file 
                FileStream fsIn = new FileStream(inputFile, FileMode.Open);//used to read your file
    
                //create a buffer (1mb) so only this amount will allocate in the memory and not the whole file
                //1048576 is 1MB in binary
                byte[] buffer = new byte[1048576];
                int read;
                try
                {
                    while ((read = fsIn.Read(buffer, 0, buffer.Length)) > 0)
                    {
                        cs.Write(buffer, 0, read);
                    }
                    fsIn.Close();
                }
                catch (Exception ex)
                {
                    await DisplayAlert("Error", "Error: " + ex.Message, "Ok");
                }
                finally
                {
                    cs.Close();
                    fsCrypt.Close();
                }
            }
    
    
     private async void BtnUpload_Clicked(object sender, EventArgs e)
            {
                //same path as inside the encrypt method
                string galleryPath = Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryPictures).AbsolutePath;
                string outputPath = Path.Combine(galleryPath + "/Vault", Path.GetFileName(file.Path) + ".aes");
    
    
                FileEncrypt(file.Path);//file is declared at the top and set in my PickImage method. This will be just be the path to the file you want to encrypt
    
                var user = await GetUser(localEmail);//own method, gets user object which I need to use 
    
                FileStream filestream = System.IO.File.OpenRead(outputPath);//get filestream to new encrypted file that we create in File Encrypt method
    
                await firebaseHelper.UploadFile(filestream, Path.GetFileName(file.Path), user.UserID);//upload the file
                var downloadurl = await firebaseHelper.GetFile(Path.GetFileName(file.Path), user.UserID);//get the download url link for this file we just uploaded, own method
                await firebaseHelper.UploadURL(Path.GetFileName(file.Path), downloadurl.ToString(), user.UserID);//own method, I do this so that I can retrieve the download url, along with user id and original file name easily later on
                await DisplayAlert("Success", "Uploaded", "OK");
                imgChoosed.Source = "";//this is just an Image preview box
            }
    
     public async Task<string> UploadFile(FileStream fileStream, string fileName, Guid userid)
            {
                try
                {
                    var fileAlreadyExists = await GetFile(fileName, userid);
                    if (fileAlreadyExists == null)
                    {
                        try
                        {
                            //this is main upload to firebase storage bit
                            var imageurl = await firebaseStorage
                            .Child("Media")
                            .Child(fileName + userid)
                            .PutAsync(fileStream);
                            return imageurl;
                        }
                        catch (Exception e)
                        {
                            Debug.WriteLine($"Error:{e}");
                            return null;
                        }
                    }
                    else
                    {
                        //below code never gets used, firebase already recognises it is duplicate and appends a number to the filename, prevents duplicates
                        try
                        {
                            var imageurl = await firebaseStorage
                            .Child("Media")
                            .Child(fileName + Guid.NewGuid() + userid)
                            .PutAsync(fileStream);
                            return imageurl;
                        }
                        catch (Exception e)
                        {
                            Debug.WriteLine($"Error:{e}");
                            return null;
                        }
                    }
                }
                catch (Exception e)
                {
                    Debug.WriteLine($"Error:{e}");
                    return null;
                } 
            }
    
    

    As for decryption, it's very similar. The way I had to do this was suggested by Jason. We download the encrypted file from Firebase Storage to our device, then pass the path to this encrypted file in the FileDecrypt method. It is a bit of a long/odd way to do it but I couldn't think of a better alternative due to the restrictions in Xamarin Forms. Methods below:

    private async void FileDecrypt(string inputFile, string outputFile)
            {
                var user = await GetUser(localEmail);//own method
                FileStream fsCrypt = new FileStream(inputFile, FileMode.Open);//open and read encrypted file
                fsCrypt.Read(user.Salt, 0, user.Salt.Length);//read salt as this the first thing we wrote when encrypting
    
                RijndaelManaged AES = new RijndaelManaged();
                AES.KeySize = 256;
                AES.BlockSize = 128;
                AES.Padding = PaddingMode.Zeros;
                var key = new Rfc2898DeriveBytes(user.Key, user.Salt, 50000);
                AES.Key = key.GetBytes(AES.KeySize / 8);
                AES.IV = key.GetBytes(AES.BlockSize / 8);
                AES.Mode = CipherMode.CFB;
                CryptoStream cs = new CryptoStream(fsCrypt, AES.CreateDecryptor(), CryptoStreamMode.Read);//used to decrypt content of the encrypted file
                FileStream fsOut = new FileStream(outputFile, FileMode.Create);//used to create your decrypted file to the path you pass in as output file
                int read;
                byte[] buffer = new byte[1048576];
                try
                {
                    while ((read = cs.Read(buffer, 0, buffer.Length)) > 0)
                    {
                        fsOut.Write(buffer, 0, read);
                    }
                }
                catch (CryptographicException ex_CryptographicException)
                {
                    await DisplayAlert("Error", "CryptographicException error: " + ex_CryptographicException.Message, "Ok");
                }
                catch (Exception ex)
                {
                    await DisplayAlert("Error", "Error: " + ex.Message, "Ok");
                }
                try
                {
                    cs.Close();
                }
                catch (Exception ex)
                {
                    await DisplayAlert("Error", "Error by closing CryptoStream: " + ex.Message, "Ok");
                }
                finally
                {
                    fsOut.Close();
                    fsCrypt.Close();
                }
            }
    
    
    private async void BtnDownload_Clicked(object sender, EventArgs e)
            {
                //output path for encrypted file
                string galleryPath = Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryPictures).AbsolutePath;
                //remove file extension
                string filename = localDownloadUrl.FileName;
                int index = filename.LastIndexOf(".");
                if (index > 0)
                {
                    filename = filename.Substring(0, index);
                }
                string outputPath = Path.Combine(galleryPath + "/Vault/", filename);
                //download encrypted file locally first
                using (var client = new WebClient())
                {
                    client.DownloadFile(localDownloadUrl.Url, outputPath);
                }
    
                //output path for decrypted file
                string galleryPath1 = Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryPictures).AbsolutePath;
                string outputPath1 = Path.Combine(galleryPath + "/Vault/", localDownloadUrl.FileName);
    
                FileDecrypt(outputPath, outputPath1);
    
                //delete extra file we downloaded
                //File.Delete(outputPath);
    
                //to show pics in gallery
                MediaScannerConnection.ScanFile(Android.App.Application.Context, new string[] { outputPath1 }, new string[] { "image / jpeg" }, null);
    
                await DisplayAlert("Success", "Image saved to gallery", "OK");
    
                //display image in image preview: TODO: change the way it works
                imageView.Source = ImageSource.FromFile(outputPath1);//this is just an image preview box I have, i just set its source to the newly decrypted file so that it displays it
    
    
                //await App.Current.MainPage.Navigation.PopModalAsync();
            }