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();
}
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();
}