@@ -28,8 +28,9 @@ namespace Teknik.Areas.Upload.Controllers | |||
[HttpPost] | |||
[AllowAnonymous] | |||
[ValidateAntiForgeryToken] | |||
public ActionResult Upload() | |||
public ActionResult Upload(string iv) | |||
{ | |||
Models.Upload upload = null; | |||
foreach (string fileName in Request.Files) | |||
{ | |||
HttpPostedFileBase file = Request.Files[fileName]; | |||
@@ -37,9 +38,15 @@ namespace Teknik.Areas.Upload.Controllers | |||
string fName = file.FileName; | |||
if (file != null && file.ContentLength > 0) | |||
{ | |||
upload = Uploader.SaveFile(file, iv); | |||
break; | |||
} | |||
} | |||
return Json(new { result = "tempURL.png" }); | |||
if (upload != null) | |||
{ | |||
return Json(new { result = new { name = upload.Url, url = Url.SubRouteUrl("upload", "Upload.Download", new { file = upload.Url }) } }, "text/plain"); | |||
} | |||
return Json(new { error = "Unable to upload file" }); | |||
} | |||
[HttpPost] |
@@ -18,9 +18,15 @@ namespace Teknik.Areas.Upload.Models | |||
public string Url { get; set; } | |||
public int FileSize { get; set; } | |||
public string FileName { get; set; } | |||
public string Hash { get; set; } | |||
public int ContentLength { get; set; } | |||
public string ContentType { get; set; } | |||
public string Key { get; set; } | |||
public string IV { get; set; } | |||
public string DeleteKey { get; set; } | |||
} |
@@ -0,0 +1,23 @@ | |||
self.addEventListener('message', function (e) { | |||
var data = e.data; | |||
importScripts(data.script); | |||
switch (data.cmd) { | |||
case 'encrypt': | |||
// encrypt the passed in file data | |||
var encrypted = CryptoJS.AES.encrypt(data.file, data.key, { iv: data.iv }); | |||
var cipherText = encrypted.toString(); | |||
self.postMessage(cipherText); | |||
break; | |||
case 'decrypt': | |||
// decrypt the passed in file data | |||
var decrypted = CryptoJS.AES.decrypt(data.file, data.key, { iv: data.iv }); | |||
var fileText = decrypted.toString(); | |||
self.postMessage(fileText); | |||
break; | |||
} | |||
}, false); |
@@ -32,18 +32,19 @@ Dropzone.options.TeknikUpload = { | |||
paramName: "file", // The name that will be used to transfer the file | |||
maxFilesize: maxUploadSize, // MB | |||
addRemoveLinks: true, | |||
autoProcessQueue: false, | |||
autoProcessQueue: true, | |||
clickable: true, | |||
accept: function (file, done) { | |||
file.done = done; | |||
encryptFile(file); | |||
encryptFile(file, done); | |||
}, | |||
init: function() { | |||
this.on("addedfile", function(file, responseText) { | |||
$("#upload_message").css('display', 'none', 'important'); | |||
}); | |||
this.on("success", function(file, response) { | |||
var name = response.result; | |||
this.on("success", function (file, response) { | |||
obj = JSON.parse(response); | |||
var name = obj.result.name; | |||
var fullName = obj.result.url; | |||
var short_name = file.name.split(".")[0].hashCode(); | |||
$("#upload-links").css('display', 'inline', 'important'); | |||
$("#upload-links").prepend(' \ | |||
@@ -52,7 +53,7 @@ Dropzone.options.TeknikUpload = { | |||
'+file.name+' \ | |||
</div> \ | |||
<div class="col-sm-3"> \ | |||
<a href='+uploadURL+'/'+name+'" target="_blank" class="alert-link">'+uploadURL+'/'+name+'</a> \ | |||
<a href="' + fullName + '" target="_blank" class="alert-link">' + fullName + '</a> \ | |||
</div> \ | |||
<div class="col-sm-3"> \ | |||
<button type="button" class="btn btn-default btn-xs generate-delete-link-'+short_name+'" id="'+name+'">Generate Deletion URL</button> \ | |||
@@ -76,8 +77,8 @@ Dropzone.options.TeknikUpload = { | |||
$("#top_msg").html('<div class="alert alert-danger alert-dismissable"><button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>'+errorMessage+'</div>'); | |||
}); | |||
this.on("totaluploadprogress", function(progress, totalBytes, totalBytesSent) { | |||
$(".progress").children('.progress-bar').css('width', progress.toFixed(2)+'%'); | |||
$(".progress").children('.progress-bar').html(progress.toFixed(2)+'%'); | |||
$(".progress").children('.progress-bar').css('width', (progress.toFixed(2) * (3/5)) + 40 +'%'); | |||
$(".progress").children('.progress-bar').html(progress.toFixed(2)+'% Uploaded'); | |||
}); | |||
this.on("queuecomplete", function() { | |||
$(".progress").children('.progress-bar').html('Complete'); | |||
@@ -85,6 +86,91 @@ Dropzone.options.TeknikUpload = { | |||
} | |||
}; | |||
// Function to encrypt a file, overide the file's data attribute with the encrypted value, and then call a callback function if supplied | |||
function encryptFile(file, callback) { | |||
// Start the file reader | |||
var reader = new FileReader(); | |||
// When the file has been loaded, encrypt it | |||
reader.onload = (function (theFile, callback) { | |||
return function (e) { | |||
// Create random key and iv | |||
var keyStr = randomString(16, '#aA'); | |||
var ivStr = randomString(16, '#aA'); | |||
var key = CryptoJS.enc.Utf8.parse(keyStr); | |||
var iv = CryptoJS.enc.Utf8.parse(ivStr); | |||
// Display encryption message | |||
$(".progress").children('.progress-bar').css('width', '20%'); | |||
$(".progress").children('.progress-bar').html('Encrypting...'); | |||
var worker = new Worker(encScriptSrc); | |||
worker.addEventListener('message', function (e) { | |||
// create the blob from e.data.encrypted | |||
theFile.data = e.data; | |||
theFile.key = keyStr; | |||
theFile.iv = ivStr; | |||
$("#iv").val(ivStr); | |||
if (callback != null) { | |||
// Finish | |||
callback(); | |||
} | |||
}); | |||
// Execute worker with data | |||
worker.postMessage({ | |||
cmd: 'encrypt', | |||
script: aesScriptSrc, | |||
key: key, | |||
iv: iv, | |||
file: e.target.result, | |||
chunkSize: 1024 | |||
}); | |||
}; | |||
})(file, callback); | |||
// While reading, display the current progress | |||
reader.onprogress = function (data) { | |||
if (data.lengthComputable) { | |||
var progress = parseInt(((data.loaded / data.total) * 100), 10); | |||
$(".progress").children('.progress-bar').css('width', (progress.toFixed(2) / 5) + '%'); | |||
$(".progress").children('.progress-bar').html(progress.toFixed(2) + '% Loaded'); | |||
} | |||
} | |||
// Start async read | |||
reader.readAsDataURL(file); | |||
} | |||
function encryptData(data, file, callback) { | |||
// 1) create the jQuery Deferred object that will be used | |||
var deferred = $.Deferred(); | |||
// Create random key and iv | |||
var keyStr = randomString(16, '#aA'); | |||
var ivStr = randomString(16, '#aA'); | |||
var key = CryptoJS.enc.Utf8.parse(keyStr); | |||
var iv = CryptoJS.enc.Utf8.parse(ivStr); | |||
// Ecrypt the file | |||
var encData = CryptoJS.AES.encrypt(data, key, { iv: iv }); | |||
// Save Data | |||
file.data = encData; | |||
file.key = keyStr; | |||
file.iv = ivStr; | |||
// Finish | |||
callback(); | |||
// 2) return the promise of this deferred | |||
return deferred.promise(); | |||
} | |||
function readBlob(file, opt_startByte, opt_stopByte) | |||
{ | |||
var start = parseInt(opt_startByte) || 0; | |||
@@ -93,10 +179,19 @@ function readBlob(file, opt_startByte, opt_stopByte) | |||
var reader = new FileReader(); | |||
reader.onload = (function (theFile) { | |||
var callback = theFile.callback; | |||
var callback = theFile.done; | |||
window.processedSize += theFile.size; | |||
return function (e) { | |||
// Ecrypt the blog | |||
window.bits.push(window.aesEncryptor.process(evt.target.result)); | |||
callback(); | |||
// Add the current size to the processed variable | |||
window.processedSize += evt.target.result.size; | |||
// If we have processed the entire file, let's do a finalize and call the callback | |||
if (window.processedSize > window.totalSize - 1) { | |||
window.bits.push(aesEncryptor.finalize()); | |||
callback(); | |||
} | |||
}; | |||
})(file); | |||
@@ -109,29 +204,7 @@ function readBlob(file, opt_startByte, opt_stopByte) | |||
} | |||
var blob = file.slice(start, stop + 1); | |||
reader.readAsArrayBuffer(blob); | |||
} | |||
function encryptFile(file) | |||
{ | |||
// INITIALIZE PROGRESSIVE ENCRYPTION | |||
window.keyStr = randomString(16, '#aA'); | |||
window.ivStr = randomString(16, '#aA'); | |||
var key = CryptoJS.enc.Utf8.parse(keyStr); | |||
var iv = CryptoJS.enc.Utf8.parse(ivStr); | |||
window.aesEncryptor = CryptoJS.algo.AES.createEncryptor(key, { iv: iv }); | |||
// LOOP THROUGH BYTES AND PROGRESSIVELY ENCRYPT | |||
window.bits = [] | |||
var startByte = 0; | |||
var endByte = 0; | |||
while (startByte <= file.size - 1) { | |||
endByte = startByte + 1024; | |||
readBlob(file, startByte, endByte); | |||
startByte = endByte; | |||
} | |||
reader.readAsDataURL(blob); | |||
} | |||
function fileDataEncrypted(done) |
@@ -25,7 +25,7 @@ namespace Teknik.Areas.Upload | |||
context.MapSubdomainRoute( | |||
"Upload.Download", | |||
"dev", | |||
"Upload/{url}", | |||
"Upload/{file}", | |||
new { controller = "Upload", action = "Download", url = string.Empty }, | |||
new[] { typeof(Controllers.UploadController).Namespace } | |||
); | |||
@@ -106,6 +106,11 @@ namespace Teknik.Areas.Upload | |||
"~/Areas/Upload/Scripts/Upload.js", | |||
"~/Scripts/bootbox/bootbox.min.js", | |||
"~/Areas/Upload/Scripts/aes.js")); | |||
BundleTable.Bundles.Add(new ScriptBundle("~/bundles/cryptoWorker").Include( | |||
"~/Areas/Upload/Scripts/EncryptionWorker.js")); | |||
BundleTable.Bundles.Add(new ScriptBundle("~/bundles/crypto").Include( | |||
"~/Areas/Upload/Scripts/aes.js")); | |||
// Register Style Bundles | |||
BundleTable.Bundles.Add(new StyleBundle("~/Content/upload").Include( | |||
"~/Content/dropzone.css", |
@@ -0,0 +1,56 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Web; | |||
using Teknik.Configuration; | |||
using Teknik.Models; | |||
namespace Teknik.Areas.Upload | |||
{ | |||
public static class Uploader | |||
{ | |||
public static Models.Upload SaveFile(HttpPostedFileBase file) | |||
{ | |||
return SaveFile(file, null, null); | |||
} | |||
public static Models.Upload SaveFile(HttpPostedFileBase file, string iv) | |||
{ | |||
return SaveFile(file, iv, null); | |||
} | |||
public static Models.Upload SaveFile(HttpPostedFileBase file, string iv, string key) | |||
{ | |||
Config config = Config.Load(); | |||
TeknikEntities db = new TeknikEntities(); | |||
// Generate a unique file name that does not currently exist | |||
string fileName = Utility.GenerateUniqueFileName(config.UploadConfig.UploadDirectory, config.UploadConfig.FileExtension, 10); | |||
// once we have the filename, lets save the file | |||
file.SaveAs(fileName); | |||
// Generate a unique url | |||
string extension = (config.UploadConfig.IncludeExtension) ? Utility.GetDefaultExtension(file.ContentType) : string.Empty; | |||
string url = Utility.RandomString(config.UploadConfig.UrlLength) + extension; | |||
while (db.Uploads.Where(u => u.Url == url).FirstOrDefault() != null) | |||
{ | |||
url = Utility.RandomString(config.UploadConfig.UrlLength) + extension; | |||
} | |||
// Now we need to update the database with the new upload information | |||
Models.Upload upload = db.Uploads.Create(); | |||
upload.DateUploaded = DateTime.Now; | |||
upload.Url = url; | |||
upload.FileName = fileName; | |||
upload.ContentLength = file.ContentLength; | |||
upload.ContentType = file.ContentType; | |||
upload.Key = key; | |||
upload.IV = iv; | |||
db.Uploads.Add(upload); | |||
return upload; | |||
} | |||
} | |||
} |
@@ -0,0 +1 @@ | |||
@model Teknik.Areas.Upload.ViewModels.UploadViewModel |
@@ -1,6 +1,8 @@ | |||
@model Teknik.Areas.Upload.ViewModels.UploadViewModel | |||
<script> | |||
var encScriptSrc = '@Scripts.Url("~/bundles/cryptoWorker")'; | |||
var aesScriptSrc = '@Scripts.Url("~/bundles/crypto")'; | |||
var generateDeleteKeyURL = '@Url.SubRouteUrl("upload", "Upload.Action", new { action= "GenerateDeleteKey" })'; | |||
var uploadURL = '@Url.SubRouteUrl("upload", "Upload.Download")'; | |||
var maxUploadSize = @(Model.Config.UploadConfig.MaxUploadSize / 100000); | |||
@@ -10,8 +12,9 @@ | |||
<div class="container"> | |||
<div class="row text-center"> | |||
<form action="@Url.SubRouteUrl("upload", "Upload.Upload")" class="dropzone" id="TeknikUpload" name="TeknikUpload" enctype="multipart/form-data"> | |||
<form action="@Url.SubRouteUrl("upload", "Upload.Action", new { action = "Upload" })" class="dropzone" id="TeknikUpload" name="TeknikUpload" enctype="multipart/form-data"> | |||
@Html.AntiForgeryToken() | |||
<input name="iv" id="iv" type="hidden" /> | |||
<div class="dz-message text-center" id="upload_message"> | |||
<div class="row"> | |||
<div class="col-sm-12"> |
@@ -9,6 +9,12 @@ namespace Teknik.Configuration | |||
{ | |||
// Max upload size in bytes | |||
public int MaxUploadSize { get; set; } | |||
// Location of the upload directory | |||
public string UploadDirectory { get; set; } | |||
// File Extension for saved files | |||
public string FileExtension { get; set; } | |||
public int UrlLength { get; set; } | |||
public bool IncludeExtension { get; set; } | |||
public UploadConfig() | |||
{ | |||
@@ -18,6 +24,10 @@ namespace Teknik.Configuration | |||
public void SetDefaults() | |||
{ | |||
MaxUploadSize = 100000000; | |||
UploadDirectory = string.Empty; | |||
FileExtension = "enc"; | |||
UrlLength = 6; | |||
IncludeExtension = true; | |||
} | |||
} | |||
} |
@@ -1,7 +1,10 @@ | |||
using System; | |||
using Microsoft.Win32; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Dynamic; | |||
using System.IO; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Web; | |||
namespace Teknik | |||
@@ -25,5 +28,65 @@ namespace Teknik | |||
} | |||
return result; | |||
} | |||
public static string RandomString(int length, string allowedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") | |||
{ | |||
const int byteSize = 0x100; | |||
var allowedCharSet = new HashSet<char>(allowedChars).ToArray(); | |||
if (byteSize < allowedCharSet.Length) throw new ArgumentException(String.Format("allowedChars may contain no more than {0} characters.", byteSize)); | |||
// Guid.NewGuid and System.Random are not particularly random. By using a | |||
// cryptographically-secure random number generator, the caller is always | |||
// protected, regardless of use. | |||
using (var rng = new System.Security.Cryptography.RNGCryptoServiceProvider()) | |||
{ | |||
var result = new StringBuilder(); | |||
var buf = new byte[128]; | |||
while (result.Length < length) | |||
{ | |||
rng.GetBytes(buf); | |||
for (var i = 0; i < buf.Length && result.Length < length; ++i) | |||
{ | |||
// Divide the byte into allowedCharSet-sized groups. If the | |||
// random value falls into the last group and the last group is | |||
// too small to choose from the entire allowedCharSet, ignore | |||
// the value in order to avoid biasing the result. | |||
var outOfRangeStart = byteSize - (byteSize % allowedCharSet.Length); | |||
if (outOfRangeStart <= buf[i]) continue; | |||
result.Append(allowedCharSet[buf[i] % allowedCharSet.Length]); | |||
} | |||
} | |||
return result.ToString(); | |||
} | |||
} | |||
public static string GenerateUniqueFileName(string path, string extension, int length) | |||
{ | |||
if (Directory.Exists(path)) | |||
{ | |||
string filename = RandomString(length); | |||
while (File.Exists(Path.Combine(path, string.Format("{0}.{1}", filename, extension)))) | |||
{ | |||
filename = RandomString(length); | |||
} | |||
return Path.Combine(path, string.Format("{0}.{1}", filename, extension)); | |||
} | |||
return string.Empty; | |||
} | |||
public static string GetDefaultExtension(string mimeType) | |||
{ | |||
string result; | |||
RegistryKey key; | |||
object value; | |||
key = Registry.ClassesRoot.OpenSubKey(@"MIME\Database\Content Type\" + mimeType, false); | |||
value = key != null ? key.GetValue("Extension", null) : null; | |||
result = value != null ? value.ToString() : string.Empty; | |||
return result; | |||
} | |||
} | |||
} |
@@ -5,6 +5,7 @@ using Teknik.Areas.Blog.Models; | |||
using Teknik.Areas.Profile.Models; | |||
using Teknik.Areas.Contact.Models; | |||
using Teknik.Migrations; | |||
using Teknik.Areas.Upload.Models; | |||
namespace Teknik.Models | |||
{ | |||
@@ -17,6 +18,7 @@ namespace Teknik.Models | |||
public DbSet<Post> Posts { get; set; } | |||
public DbSet<Comment> BlogComments { get; set; } | |||
public DbSet<Contact> Contact { get; set; } | |||
public DbSet<Upload> Uploads { get; set; } | |||
protected override void OnModelCreating(DbModelBuilder modelBuilder) | |||
{ | |||
@@ -27,6 +29,7 @@ namespace Teknik.Models | |||
modelBuilder.Entity<Post>().ToTable("Posts"); | |||
modelBuilder.Entity<Comment>().ToTable("BlogComments"); | |||
modelBuilder.Entity<Contact>().ToTable("Contact"); | |||
modelBuilder.Entity<Upload>().ToTable("Uploads"); | |||
base.OnModelCreating(modelBuilder); | |||
} |
@@ -168,6 +168,7 @@ | |||
<Compile Include="Areas\Upload\Controllers\UploadController.cs" /> | |||
<Compile Include="Areas\Upload\Models\Upload.cs" /> | |||
<Compile Include="Areas\Upload\UploadAreaRegistration.cs" /> | |||
<Compile Include="Areas\Upload\Uploader.cs" /> | |||
<Compile Include="Areas\Upload\ViewModels\UploadViewModel.cs" /> | |||
<Compile Include="Configuration\BlogConfig.cs" /> | |||
<Compile Include="Configuration\Config.cs" /> | |||
@@ -208,6 +209,7 @@ | |||
<Content Include="Areas\Home\Scripts\Home.js" /> | |||
<Content Include="Areas\Profile\Scripts\Profile.js" /> | |||
<Content Include="Areas\Upload\Scripts\aes.js" /> | |||
<Content Include="Areas\Upload\Scripts\EncryptionWorker.js" /> | |||
<Content Include="Areas\Upload\Scripts\Upload.js" /> | |||
<Content Include="Content\bootstrap-markdown.min.css" /> | |||
<Content Include="Content\bootstrap-theme.css" /> | |||
@@ -278,6 +280,7 @@ | |||
<Content Include="Areas\Error\Views\Error\Exception.cshtml" /> | |||
<Content Include="Areas\Error\Views\Error\General.cshtml" /> | |||
<Content Include="Areas\Error\Views\Error\Http500.cshtml" /> | |||
<Content Include="Areas\Upload\Views\Upload\Download.cshtml" /> | |||
<None Include="Properties\PublishProfiles\Teknik Dev.pubxml" /> | |||
<None Include="Scripts\jquery-2.1.4.intellisense.js" /> | |||
<Content Include="Scripts\bootbox\bootbox.min.js" /> |